如何在标准C++中用计算型goto将动态调度提速20%
Great question! Computed gotos are a fantastic optimization for VM main loops—they cut down on branch prediction overhead and eliminate the redundant checks that come with standard switch statements. Since standard C++ doesn't support label pointers, let's walk through two solid approaches to get nearly the same performance, plus a clean way to optimize virtual function calls for your VM.
The closest standard equivalent to computed gotos is a function pointer jump table. Instead of relying on a switch to dispatch each instruction, we map each opcode directly to a function that handles it. Compilers like GCC and Clang will optimize this into a direct jump (just like computed gotos) with minimal overhead.
Here's how to refactor your example VM to use this approach:
#include <iostream> enum class Opcode { HALT, INC, DEC, BIT_LEFT, BIT_RIGHT, RET }; int result = 0; constexpr size_t opcode_count = static_cast<size_t>(Opcode::RET) + 1; // Define instruction handlers void handle_halt() {} void handle_inc() { ++result; } void handle_dec() { --result; } void handle_bit_left() { result <<= 1; } void handle_bit_right() { result >>= 1; } void handle_ret() { std::cout << result; std::exit(0); } // Jump table: map opcodes to handler functions using HandlerFunc = void(*)(); constexpr HandlerFunc jump_table[opcode_count] = { handle_halt, handle_inc, handle_dec, handle_bit_left, handle_bit_right, handle_ret }; int main() { Opcode program[] = { Opcode::INC, Opcode::BIT_LEFT, Opcode::BIT_LEFT, Opcode::BIT_LEFT, Opcode::INC, Opcode::INC, Opcode::RET }; for (Opcode instruction : program) { // Direct dispatch via jump table—no switch, no redundant checks size_t opcode_idx = static_cast<size_t>(instruction); jump_table[opcode_idx](); } return 0; }
Why this works:
- The jump table is a compile-time constant array of function pointers, so the compiler can optimize the dispatch to a single memory lookup + jump—almost identical to computed gotos.
- No more
switch-required boundary checks or branch prediction overhead from multiple case labels. - Performance is typically within 5-10% of computed gotos, which is a huge improvement over the original
switchversion.
If you want to avoid global variables (like result), you can wrap everything in a class and use member function pointers, or pass a context struct by reference to each handler.
Virtual functions introduce overhead from vtable lookups and indirect jumps. For VM instruction handlers (or any polymorphic container), static polymorphism via CRTP (Curiously Recurring Template Pattern) eliminates this overhead by resolving calls at compile time.
CRTP-based Static Polymorphism
Here's a clean, extensible setup for instruction handlers using CRTP:
#include <iostream> #include <array> enum class Opcode { HALT, INC, DEC, BIT_LEFT, BIT_RIGHT, RET }; constexpr size_t opcode_count = static_cast<size_t>(Opcode::RET) + 1; // Base CRTP class for instruction handlers template <typename Derived> struct InstructionHandler { void handle(Opcode opcode, int& result) { // Dispatch to the derived class's opcode-specific methods constexpr std::array<void(Derived::*)(int&), opcode_count> dispatch_table = { &Derived::halt, &Derived::inc, &Derived::dec, &Derived::bit_left, &Derived::bit_right, &Derived::ret }; auto handler = dispatch_table[static_cast<size_t>(opcode)]; static_cast<Derived*>(this)->*handler(result); } }; // Concrete handler implementation struct VMHandler : InstructionHandler<VMHandler> { void halt(int&) {} void inc(int& result) { ++result; } void dec(int& result) { --result; } void bit_left(int& result) { result <<= 1; } void bit_right(int& result) { result >>= 1; } void ret(int& result) { std::cout << result; std::exit(0); } }; int main() { Opcode program[] = { Opcode::INC, Opcode::BIT_LEFT, Opcode::BIT_LEFT, Opcode::BIT_LEFT, Opcode::INC, Opcode::INC, Opcode::RET }; VMHandler handler; int result = 0; for (Opcode instruction : program) { handler.handle(instruction, result); } return 0; }
Macro for Easy Extension
If you have dozens of opcodes, a macro can simplify defining the dispatch table and handler methods:
#define DEFINE_OPCODE_HANDLER(opcode, func) \ void func(int& result) #define POPULATE_DISPATCH_TABLE(opcode, func) \ &Derived::func // Usage in VMHandler: DEFINE_OPCODE_HANDLER(Opcode::INC, inc) { ++result; } DEFINE_OPCODE_HANDLER(Opcode::DEC, dec) { --result; } // ... and so on
Why this beats virtual functions:
- All calls are resolved at compile time—no vtable lookups, no runtime overhead.
- The dispatch table is a compile-time constant, so the compiler can optimize it to direct jumps.
- It's fully extensible: add a new opcode by defining a handler method and updating the dispatch table (or macro).
Another option is to use std::variant with std::visit—modern compilers optimize std::visit into a jump table for small variant sizes, which is also overhead-free.
Final Notes
- For VM interpreters, the function pointer jump table is the closest you'll get to computed gotos in standard C++—it's fast, simple, and widely compatible.
- Static polymorphism via CRTP (or
std::variant) is the best way to eliminate virtual function overhead for polymorphic handler containers.
内容的提问来源于stack exchange,提问作者user11313931

