interpreters
Bytecode interpreter and JIT compiler skill for implementing language runtimes in C/C++. Use when designing bytecode dispatch loops (switch, computed goto, threaded code), implementing stack-based or register-based VMs, adding a simple JIT using mmap/mprotect, or understanding performance trade-offs
By mohitmishra786 · 433 installs
npx skills add mohitmishra786/low-level-dev-skills --skill interpreters
Source repository · Upstream listing
Interpreters and Bytecode VMs
Purpose
Guide agents through implementing efficient bytecode interpreters and simple JITs in C/C++: dispatch strategies, VM architecture choices, and performance patterns.
Triggers
"How do I implement a fast bytecode dispatch loop?"
"What is the difference between switch dispatch and computed goto?"
"How do I implement a register based vs stack based VM?"
"How do I add basic JIT compilation to my interpreter?"
"Why is my interpreter slow?"
Workflow
1. VM architecture choice
Style Description Examples
Stack based Operands on a value stack; compact bytecode JVM, CPython, WebAssembly
Register based Operands in virtual registers; fewer instructions Lua 5+, Dalvik
Direct threading Each "instruction" is a function call Some Forth implementations
Continuation passing Interpreter functions return continuations Academic
Stack based: easier to implement, compile to; code generation is simpler. More instructions per expression.
Register based: fewer dispatch iterations; needs register allocation in the compiler; better cache behaviour for complex expressions.
2. Dispatch loop strategies
Switch dispatch (simplest, baseline)
Problem: switch compiles to a single indirect branch from a jump table. Modern CPUs can mispredict it heavily because the same indirect branch is used for all opcodes.
Computed goto (GCC/Clang extension — fastest portable approach)
Each opcode ends with its own indirect branch. The CPU can train the branch predictor per opcode, dramatically improving prediction rates.
Note: &&label is a GCC/Clang extension, not standard C. Use ifdef GNUC to guard and fall back to switch for other compilers.
Direct threaded code (most aggressive)
Each bytecode word is a function pointer or label address; the VM is the fetch decode execute loop itself.
3. Value representation
Tagged pointer: Store type tag in low bits of pointer (pointer alignment guarantees ≥ 2 bits free):
NaN boxing (64 bit): Store non double values in NaN bit patterns:
Used by V8 (formerly), LuaJIT, JavaScriptCore.
4. Stack management
5. Inline caching (IC)
Inline caching speeds up property lookups and method dispatch by caching the last observed type at each call site.
Polymorphic IC (PIC): cache up to N (typ. 4) type method pairs.
6. Simple JIT (mmap + machine code)
For x86 64: allocate executable memory, write machine code bytes, call it.
On macOS Apple Silicon (M series): use pthread jit write protect np() or MAP JIT flag.
7. Performance tips
1. Dispatch: Use computed goto over switch on GCC/Clang
2. Values: Use NaN boxing or tagged pointers; avoid boxing/unboxing in hot paths
3. Stack: Keep stack pointer in a callee saved register ( register Value sp asm("r15") — GCC global register variable)
4. Locals access: Keep frequently accessed locals in VM registers (struct fields), not stack
5. Profiling: Use perf or sampling to find dispatch overhead vs actual work
6. Specialisation: Generate specialised handler variants for common type combinations (int+int add vs generic add)
7. Trace recording: Trace JITs (LuaJIT approach) compile hot traces instead of full functions
For a benchmark of dispatch strategies, see [references/benchmarks.md](references/benchmarks.md).
Related skills
Use skills/profilers/linux perf to profile the interpreter dispatch loop
Use skills/low level programming/assembly x86 to understand JIT output
Use skills/runtimes/fuzzing to fuzz the bytecode parser/loader
Use skills/compilers/llvm for LLVM IR based JIT (MCJIT / ORC JIT)