Computed goto for efficient dispatch tables (2012)
eli.thegreenplace.net
eli.thegreenplace.net
20 GOTO (30 + (start%*10))
I used to use it in game loops to call different procedures for character control based on the type of character. Just needed a bit of careful tuning of the character type integer and the line numbers.Required very precise timing. Something Basic didn’t have.
So we used for loops.
Need a 2ms loop. 5,000 iterations. Etc.
Worked fine for years, until we couldn’t order the same cpus. Then machine ran way to fast.
So we set it up so that at boot time, it would calibrate how many loops equaled various milliseconds. Problem solved.
Was a really weird early job.
So if you tested sometime between Haswell and Skylake, there was a period when there was no noticeable difference in cgoto vs a switch dispatch[1].
Then Intel got caught up in Meltdown, which is how it was speculating through branches and we're back to 2010 speeds for the switch dispatch, because the branch prediction won't load up the cache (the instruction cache) or start decoding before the conditional is entirely processed.
1st: implement the ops basic block as linked list, not an array of opcodes. Thus the next op can be called directly. No lookup and no indirect call.
2nd: inline the op insn into the basic block array, like a jit or just llvm byte code or similar. No jumps, everything stays in the i-cache.
There are more, even faster options, like luajit. Dispatch tables are pretty desperate in 2020. With 20 ops and short op insns (as in lua) everything fits into the icache. With fixed registers you can be pretty fast.
goto *(void const *) *virtual_pc_in_register;
That gets translated to an indirect jump. There's no lookup in a dispatch table but that's still an indirect branch.This is actually marginally better than LuaJIT which does jump * (dispatch_table + *virtual_pc_in_register) but LuaJIT does this so it can swap the dispatch tables to switch to trace recording.
1) If you have a basic block of bytecodes, how does a linked list help? You still have an indirect jump to actually execute the instructions for the bytecode op. Or do you mean having a linked-list of direct jumps somehow?
2) Do you mean template JIT by replacing the jump to the bytecode op with implementation of the op?
Do you mean the tracing compiler? LuaJIT's interpreter still uses a dispatch table.
[jonesforth.s] is a great search term for a well-documented Forth implementation explaining how threaded code works.
I'm actually surprised I've never seen a system that uses tiny JIT stubs to implement the interpreter portion of a modern hybrid interpreter and JIT system. The class loader would initially "JIT" each method as a function call to the interpreter function followed immediately by a return, with a pointer to a memory-mapped buffer of the bytecode after the return instruction. The interpreter function would look at its return address (which points to the JIT stub's return instruction) and add a constant offset to find the pointer to the bytecode to interpret. This avoids the need for an extra pointer indirection or conditional branch in your hot path method dispatch. Granted, in your really hot paths, you hope your JIT inlines everything.
Op linked lists are bad for the icache, but use all direct calls, which are much better cachable. The best is when you sort the list to be adjacent. Then you have the best of both.
You assume the development bandwidth exists to implement JIT runtimes for all small interpreters/VMs. Transforming VM bytecode into host machine code isn't a new concept, quite the opposite. The development cost to do so generally outweighs the benefits, especially in the beginning of development.
Plus, fortunately, such a refactor is generally unobtrusive, so computed goto is by far a more attractive option than implementing a JIT runtime.
The reason I went with functions instead in C++ is to allow plugging new opcodes into the instruction set without updating the dispatch loop.
If this is a switch that is reasonably defended (internal API, etc.) then leaving off a default in a fixed enumeration type switch (where you want to hit _every_ switch case value possible) is ideal.
Most modern processors will warn (or -Werror, as everyone should be using these days) if one of the enum constants is missing, reducing the surface area for logic bugs.
If you are really paranoid about out-of-bounds values in a switch statement, then insert an assert() after the statement itself with a negated string literal, e.g. assert(!"invalid switch value");
This is a pretty reasonable approach for code that, again, is reasonably defended against faulty inputs or working with some degree of guarantee. If I'm not mistaken, often times such switch statements are even optimized to jump tables at compile time, too - namely if there are small/few/no gaps in the case values.
I'd be curious to hear why it was never standardized. It seems like low hanging fruit to me.