虛擬機位元組碼解釋哪種模式速度更快?

@無名俠 @古河麵包店 @RednaxelaFX 一種是常見的指令分派,另一種是操作碼映射(利用函數指針數組)


題主說的:

一種是常見的指令分派,另一種是操作碼映射(利用函數指針數組)

是分別是switch threading的分派和循環方式,與查表分派么。

如果題主說的比較雙方分別是用C++來寫的下面兩種代碼:

void run(Context* ctx, Function* f) {
int8_t* pc = f-&>code();
while (true) {
int8_t opcode = *pc++;
switch (opcode) {
case NOP:
break;
case XXX:
// do xxx
break;
// ...
}
}
}

typedef bool (*opcode_handler)(Context*, int8_t*);

bool handle_nop(Context* ctx, int8_t* pc) { }

static const opcode_handler handlers[] = {
handle_nop,
// ...
};

void run(Context* ctx, Function* f) {
int8_t* pc = f-&>code();
bool should_break = false;
while (!should_break) {
int8_t opcode = *pc++;
should_break = handlers[opcode](ctx, pc);
}
}

這倆哪個更快的話,那其實不但取決於這個虛擬指令集的每個處理代碼有多複雜,還取決於這裡用的C++編譯器有多優化。要知道後者是完全有可能被C++編譯器優化到很神奇的形式的。

如果拋開編譯器優化不談(但拋開就不現實了…),字面上看這兩種寫法的性能差異的話,如果這個指令集很簡單,每條指令的handler都非常短小的話,那麼多半會是前者更快;如果這個指令集很複雜,有很多指令的handler都非常複雜的話,那麼後者有可能會更快。這裡會涉及編譯出來的代碼對i-cache的命中率的影響。脫離實際代碼很難做有效的具體分析。

而更常見的寫法是用一些編譯器擴展語法,例如GCC/Clang支持的 Labels as Values 功能,來實現所謂computed goto方式實現的token-threading解釋器循環。這種寫法會用到label數組,跟函數數組相比,label數組保證引用的跳轉目標肯定在同一個函數里,跳轉過去的開銷通常小於函數調用的開銷。

關於computed goto的例子,請跳這個傳送門:Computed goto for efficient dispatch tables

放幾個有一丁點相關的傳送門:

  • 編譯器具體實現中比較巧妙的思想有哪些? - RednaxelaFX的回答 - 知乎
  • Zend opcode是如何被執行的?是要編譯為機器碼再執行嗎? - RednaxelaFX的回答 - 知乎
  • 請問LLVM IR中 br 和 indirectbr 指令的區別是什麼? - RednaxelaFX的回答 - 知乎
  • Android 5.0的ART是如何為方法調用開闢棧空間的? - RednaxelaFX的回答 - 知乎


推薦閱讀:

TAG:演算法 | 解釋器 | 精簡指令集RISC | 指令 |