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)