1 回答

TA貢獻1820條經驗 獲得超9個贊
比較稀疏的switch(指case的值之間相差得比較大)確實是需要一次次地比較才能選定到底應該進入哪個分支。不過CPython的這個ceval.c里的switch是非常稠密的,case之間的值相差都是1,因此流行的編譯器(gcc/msvc/llvm-clang)都能夠將這個switch轉化為簡單的indirect branch,比如以x86-64,linux,gas syntax為例:
cmp $MAX_OP, %opcodeja .handle_max_op jmp *.op_dispatch_table(,%opcode,8)
翻譯成C的話,大致意思是這樣:
static void *op_dispatch_table[] = { &&handle_NOP, &&handle_LOAD_FAST, // etc etc...};if (opcode > MAX_OP) { goto *handle_max_op; }else { goto *op_dispatch_table[opcode]; }
所以其實并不會像你所說的那樣逐條比較。
Interpreter的優化是很有意思的。switch看似高效,但是實際上由于生成的代碼會在同一個地方進行所有的indirect branch(分支目標可以是任何地方),處理器的分支預測就變得毫無用處了。
分支預測在CPython這種基于棧的解釋器里非常重要,這是因為大部分的OPCODE都較短,opcode dispatch(也就是分支預測)花的時間經常能占到大頭。在大家常用的Sandy Bridge處理器里,分支預測失敗相當于15個cycle,而IPC(每cycle能執行的CPU指令)在分支預測成功的情況下一般能達到3。相比之下,LOAD_FAST這種小型的OPCODE僅僅只用到了不到10個CPU指令.. 也就是說,分支預測所花的時間,甚至能占到整個code運行時間的80%。
因此,CPython使用了另外兩個優化技巧,一是對常用連續指令的預測,二是如果編譯器支持則使用indirect threading。
連續指令的預測,指的是由于Python中,有很多指令經常成對出現(比如在if x < y then xxx else xxx
里會出現COMPARE_OP -> POP_JUMP_IF_FALSE
)。這種情況下,前一個OPCODE并不需要依靠switch來執行后一個OPCODE,它可以自己跳到后一個OPCODE上去,需要做的只是檢查一下后一個OPCODE是不是自己所想要的而已。這里需要添加兩個分支,但是這兩個分支一個是條件判斷,一個是直接跳過去,所以處理器的分支預測可以在這里發揮作用。在ceval.c
里,如果你看到了PREDICT(...)
的字樣,那就說明這里有連續指令的預測了。
indirect threading
,指的是將indirect branch放在每個OPCODE處理的結尾部分。這樣一來,每個OPCODE就會獲得處理器針對自己下一個指令的分支預測。雖然這依然是indirect branch
,但是由于每個OPCODE分開預測,這大大提高了預測的準確度。CPython2.7并沒有用到這個優化。CPython3+會根據編譯器支持與否,來開關這個選項。
CPython的解釋器,經過多年的打磨,優化那是剛剛的。
添加回答
舉報