| name | pall-jit-mastery |
| description | Write high-performance JIT compilers and interpreters in the style of Mike Pall, creator of LuaJIT. Emphasizes trace-based compilation, aggressive optimization, and understanding CPU microarchitecture. Use when building JITs, interpreters, or any code where every cycle counts. |
| tags | jit, luajit, trace-compilation, interpreter, bytecode, optimization, dynamic-languages, performance, low-level |
Mike Pall Style Guide
Overview
Mike Pall created LuaJIT, widely considered one of the most impressive JIT compilers ever written. A single developer achieved performance competitive with production JVMs while maintaining a tiny codebase. His work demonstrates that deep understanding of hardware and algorithms beats large teams with brute force.
Core Philosophy
"Measure, don't guess."
"The fastest code is code that doesn't run."
"Understand your hardware or it will humble you."
Pall believes in ruthless optimization through deep understanding—knowing the CPU so well that you can predict cycle counts by reading assembly.
Design Principles
-
Trace-Based Compilation: Optimize what actually runs, not what might run.
-
Microarchitecture Awareness: Write code for the real CPU, not an abstract machine.
-
Minimal Abstraction: Every layer costs cycles.
-
Data-Oriented Design: Memory layout dominates performance.
When Writing Performance-Critical Code
Always
- Benchmark before and after every change
- Understand the generated assembly
- Profile to find actual hot spots
- Consider cache behavior for every data structure
- Know your target CPU's pipeline
- Test on multiple architectures
Never
- Assume an optimization helps without measuring
- Ignore branch prediction effects
- Use abstractions that hide memory access patterns
- Optimize cold code paths
- Trust microbenchmarks for macro decisions
- Assume compiler optimizations happen
Prefer
- Trace compilation over method compilation
- Linear memory access over pointer chasing
- Branchless code in hot paths
- Tables over computed branches
- Inline caching for polymorphic calls
- Specialized code paths over generic
Code Patterns
Trace-Based Compilation
typedef struct Trace {
uint32_t *mcode;
IRIns *ir;
uint16_t nins;
uint16_t nk;
SnapShot *snap;
uint16_t nsnap;
struct Trace *link;
} Trace;
void record_instruction(JitState *J, BCIns ins) {
switch (bc_op(ins)) {
case BC_ADDVN:
TRef tr = emitir(IR_ADD, J->slots[bc_a(ins)],
lj_ir_knum(J, bc_d(ins)));
J->slots[bc_a(ins)] = tr;
break;
}
}
IR Design for Speed
typedef struct IRIns {
uint16_t op;
uint16_t op1;
uint16_t op2;
uint16_t prev;
} IRIns;
#define IRREF_BIAS 0x8000
#define irref_isk(r) ((r) < IRREF_BIAS)
Side Exits and Guards
void emit_guard(JitState *J, IRType expected, TRef tr) {
IRIns *ir = &J->cur.ir[tref_ref(tr)];
if (ir->t != expected) {
emitir(IR_GUARD, tr, expected);
snapshot_add(J);
}
}
typedef struct SnapShot {
uint16_t ref;
uint8_t nslots;
uint8_t topslot;
uint32_t *map;
} SnapShot;
Assembly-Level Optimization
static void emit_rr(ASMState *as, x86Op op, Reg r1, Reg r2) {
if (r1 >= 8 || r2 >= 8) {
*--as->mcp = 0x40 | ((r1 >> 3) << 2) | (r2 >> 3);
}
*--as->mcp = 0xc0 | ((r1 & 7) << 3) | (r2 & 7);
*--as->mcp = op;
}
void ra_allocate(ASMState *as) {
for (IRRef ref = as->curins; ref >= as->stopins; ref--) {
IRIns *ir = &as->ir[ref];
Reg dest = ra_dest(as, ir);
ra_left(as, ir, dest);
ra_right(as, ir);
}
}
Memory Access Patterns
struct Node {
struct Node *next;
int type;
union {
double num;
struct String *str;
} value;
};
struct ValueArray {
uint8_t *types;
TValue *values;
size_t count;
};
for (size_t i = 0; i < arr->count; i++) {
if (arr->types[i] == TYPE_NUMBER) {
sum += arr->values[i].n;
}
}
Inline Caching
typedef struct InlineCache {
uint32_t shape_id;
uint16_t offset;
uint16_t _pad;
} InlineCache;
TValue get_property_cached(Object *obj, String *key, InlineCache *ic) {
if (likely(obj->shape_id == ic->shape_id)) {
return obj->slots[ic->offset];
}
uint16_t offset = shape_lookup(obj->shape, key);
ic->shape_id = obj->shape_id;
ic->offset = offset;
return obj->slots[offset];
}
Branch Prediction Awareness
for (int i = 0; i < n; i++) {
if (data[i] > threshold) {
sum += data[i];
}
}
for (int i = 0; i < n; i++) {
int mask = -(data[i] > threshold);
sum += data[i] & mask;
}
qsort(data, n, sizeof(int), compare);
for (int i = 0; i < n && data[i] <= threshold; i++) {
}
for (int i = 0; i + 4 <= n; i += 4) {
sum += data[i];
sum += data[i + 1];
sum += data[i + 2];
sum += data[i + 3];
}
Type Specialization
TValue generic_add(TValue a, TValue b) {
if (tvisnum(a) && tvisnum(b)) {
return numV(numV(a) + numV(b));
} else if (tvisstr(a) || tvisstr(b)) {
return concat(tostring(a), tostring(b));
}
}
double specialized_add_nn(double a, double b) {
return a + b;
}
Performance Mental Model
CPU Pipeline Awareness
══════════════════════════════════════════════════════════════
Latency (cycles) Operation
────────────────────────────────────────────────────────────
1 Register-to-register ALU
3-4 L1 cache hit
~12 L2 cache hit
~40 L3 cache hit
~200 Main memory
~10-20 Branch mispredict penalty
~100+ Page fault
Key insight: Memory is the bottleneck
Computation is nearly free by comparison
Optimize for memory access patterns first
Mental Model
Pall approaches optimization by asking:
- What's the hot path? Trace it, optimize it
- What does the assembly look like? If you can't read it, you can't optimize it
- Where are the cache misses? Memory dominates everything
- What are the branch patterns? Predictable branches are free
- Can I specialize? Generic code is slow code
Signature Pall Moves
- Trace compilation: JIT what runs, not what's written
- Compact IR: 8-byte instructions, index-based references
- Backwards register allocation: See all uses before deciding
- NaN boxing: Encode type and value in 64-bit doubles
- Side exit snapshots: Restore interpreter state precisely
- Assembly-level thinking: Know the cost of every instruction
- FFI that's actually fast: C calls without overhead