之前一直在研究執行代碼之類的問題,現在正好有機會和大家討論一下.
這一篇重要說明解釋器的基本工作過程和JSC的核心組件的實現。
作爲一個語言,就像人在的平常交流時一樣,當接收到信息後,包括兩個過程:先懂得再行爲。懂得的過程就是語言解析的過程,行爲就是根據解析的結果執行對應的行爲。在計算機範疇,懂得就是編譯或解釋,這個已被研究的很透徹了,並且有了工具來幫助。而執行則千變萬化,也是性能優化的重心。下面就來看看JSC是如何來懂得、執行JavaScript腳本的。
解釋器工作過程
JavaScriptCore基本的工作過程如下:
對於一個解釋器,首先必需要明白所支撐的語言, JSC所支撐的是EMCAScript-262規範。
詞法分析和語法分析就是懂得的過程,將輸入的文本轉爲一種它可以懂得的語義情勢(抽象語法樹), 或者更進一步的生成供後續應用的中間代碼(字節碼,ByteCode)。
解釋器就是擔任執行解析輸出的結果。正因爲執行是優化的重心,所以有JIT來提高執行效能。根據資料,V8還會優化Parser的輸出,省去了bytecode, 當解釋器有能力直接基於AST執行。
詞法分析及語法分析,最著名的工具就是lex/yacc,以及後繼者flex/bison(The LEX&YACC Page)。它們爲很多軟件提供了語言或文本解析的功能,相當壯大,也很有趣。雖然JavaScriptCore並沒有應用它們,而是自行編寫實現的,但基本思路是相似的。
詞法分析(lexer),其實就是一個掃描器,依據語言的定義,提取出源文件中的內容變爲一個個語法可以識別的token,比如關鍵字,操作符,常量等。在一個文件中定義好規矩就可以了。
語法分析(paser), 它的功能就是根據語法(token的順序組合),識別出不同的語義(目標操作)。
比如:
i=3;
經過lexer可能被識別爲以下的tokens:
VARIABLE EQUAL CONSTANT END
經過parser一分析,就瞭解這是一個"賦值操作,向變量i賦值常量3"。隨後再調用對應的操作加以執行。
如果你對lexer和parser還不太熟悉,可參考的資料很多,這裏有一個基本的入門指引:Yacc與Lex快速入門。
關於解釋器和JIT的說明在第3節。
執行的基本環境(Register-based VM)
JSC解析生成的代碼放到一個虛擬機上來執行(廣義上講JSC主身就是一個虛擬機)。JSC應用的是一個基於寄存器的虛擬機(register-based VM),另一種實現方式是基於棧的虛擬機(stack-based VM)。兩者的差異可以簡略的懂得爲指令集傳遞參數的方式,是應用寄存器,還是應用棧。
絕對於基於棧的虛擬機,因爲不需要頻繁的壓、出棧,以及對三元操作的支撐,register-based VM的效率更高,但可移植性絕對弱一些。
所謂的三元操作符,其中add就是一個三元操作,
add dst, src1, src2
功能是將src1與src2相加,將結果保存在dst中。dst, src1,src2都是寄存器。
爲了便利和<<深刻懂得Java虛擬機>>中的示例停止比較,也利用JSC輸出以下腳本的ByteCode如下:
[ 0] enter
[ 1] mov r0, Cell: 0133FC40(@k0)
[ 4] put_by_id r0, a(@id0), Int32: 100(@k1)
[ 13] mov r0, Cell: 0133FC40(@k0)
[ 16] put_by_id r0, b(@id1), Int32: 200(@k2)
[ 25] mov r0, Cell: 0133FC40(@k0)
[ 28] put_by_id r0, c(@id2), Int32: 300(@k3)
[ 37] resolve_global r0, a(@id0)
[ 43] resolve_global r1, b(@id1)
[ 49] add r0, r0, r1
[ 54] resolve_global r1, c(@id2)
[ 60] mul r0, r0, r1
[ 65] ret r0
*參考: JSC字節碼規格 (WebKit沒有及時更新,只做爲參考,最新的內容還是要看代碼.)
而基於棧的虛擬機的生成的字節碼如下:
0: bipush 100
2: istore_1
3: sipush 200
6: istore_2
7: sipush 300
10: istore_3
11: iload_1
12: iload_2
13: iadd
14: iload_3
15: imul
16: ireturn
可以幫助懂得它們之間的差異。
核心組件
*這部份基本上譯自WebKit官網的JavaScriptCore說明的前半部份。
JavaScriptCore 是一個正在演進的虛擬機(virtual machine), 包括了以下模塊: lexer, parser, start-up interpreter (LLInt), baseline JIT, and an optimizing JIT (DFG).
Lexer 擔任詞法解析(lexical analysis) , 就是將腳本分解爲一系列的tokens. JavaScriptCore的 lexer是手動撰寫的,大部份代碼在parser/Lexer.h 和 parser/Lexer.cpp 中.
Parser 處置語法分析(syntactic analysis), 也就是基於來自Lexer的tokens創立語法樹(syntax tree). JavaScriptCore 應用的是一個手動編寫的遞歸下降解析器(recursive descent parser), 代碼位於parser/JSParser.h 和 parser/JSParser.cpp .
LLInt, 全稱爲Low Level Interpreter, 擔任執行由Paser生成的字節碼(bytecodes). 代碼在llint/ 目錄裏, 應用一個可移植的彙編實現,也被爲offlineasm (代碼在offlineasm/目錄下), 它可以編譯爲x86和ARMv7的彙編以及C代碼。LLInt除了詞法解析和語法解釋外,JIT編譯器所執行的調用、棧、以及寄存器轉換都是基本沒有啓動開銷(start-up cost)的。比如,調用一個LLInt函數就和調用一個已被編譯原始代碼的函數相似, 除非機器碼的入口恰是一個共用的LLInt Prologue(公共函數頭,shared LLInt prologue). LLInt還包括了一些優化,比如應用inline cacheing來加速屬性訪問.
Baseline JIT 在函數被調用了6次,或者某段代碼循環了100次後(也可能是一些組合,比如3次帶有50次枚舉的調用)就會觸發Baseline JIT。這些數字只是大概的估計,實際上的啓發(heuristics)過程是依賴於函數大小和當時內存狀態的。當JIT卡在一個循環時,它會執行On-Stack-Replace(OSR)將函數的全部調用者從新指向新的編譯代碼。Baseline JIT同時也是函數進一步優化的後備,如果沒法優化代碼時,它還會通過OSR調整到Baseline JIT. BaseLine JIT的代碼在 jit/ . 基線JIT也爲inline caching執行幾乎全部的堆訪問。
無論是LLInt和Baseline JIT者會收集一些輕量級的性能信息,以便擇機到更高一層級(DFG)執行。收集的信息包括近來從參數、堆,以及返回值中的數據。另外,全部inline caching也做了些處置,以便利DFG停止類型判斷,例如,通過查詢inline cache的狀態,可以檢測到應用特定類理停止堆訪問的頻率。這個可以用於決定是不是進入DFG (文中稱這個行爲叫speculation, 有點賭一把的意思,能優化獲得更高的性能最好,不然就退回來)。在下一節中側重講述JavaScriptCore類型推斷。
DFG JIT 在函數被調用了至少60次,或者代碼循環了1000次,就會觸發DFG JIT。一樣,這些都是近似數,整個過程也是趨向於啓發式的。DFG積極地基於前面(baseline JIT&Interpreter)收集的數據停止類型揣測,這樣就可以儘早獲得類型信息(forward-propagate type information),從而減少了大批的類型檢查。DFG也會自行停止揣測,比如爲了啓用inlining, 可能會將從heap中加載的內容識別出一個已知的函數對象。如果揣測失敗,DFG取消優化(Deoptimization),也稱爲"OSR exit". Deoptimization可能是同步的(某個類型檢測分支正在執行),也可能是異步的(比如runtime視察到某個值變化了,並且與DFG的假設是衝突的),後者也被稱爲"watchpointing"。 Baseline JIT和DFG JIT共用一個雙向的OSR:Baseline可以在一個函數被頻繁調用時OSR進入DFG, 而DFG則會在deoptimization時OSR回到Baseline JIT. 反覆的OSR退出(OSR exits)還有一個統計功能: DFG OSR退出會像記載發生頻率一樣記載下退出的理由(比如對值的類型揣測失敗), 如果退出一定次數後,就會引發從新優化(reoptimization), 函數的調用者會從新被定位到Baseline JIT,然後會收集更多的統計信息,或許根據需要再次調用DFG。從新優化應用了指數式的回退策略(exponential back-off,會越來越來)來應對一些奇葩代碼。DFG代碼在dfg/.
任何時候,函數, eval代碼塊,以及全局代碼(global code)都可能會由LLInt, Baseline JIT和DFG三者同時運行。一個極端的例子是遞歸函數,因爲有多個stack frames,就可能一個運行在LLInt下,另一個運行在Baseline JIT裏,其它的可能正運行在DFG裏。更爲極端的情況是當從新優化在執行過程被觸發時,就會出現一個stack frame正在執行本來舊的DFG編譯,而另一個則正執行新的DFG編譯。爲此三者計劃成維護雷同的執行語義(execution semantics), 它們的混合應用也是爲了帶來顯著的效能晉升。
*如果想要視察它們的工作,可以在WebKit中的子工程jsc的jsc.cpp中,應用JSC::Options添加一部份log輸出.
參考閱讀:
虛擬機隨談(一): 解釋器,樹遍歷解釋器,基於棧與基於寄存器,大雜燴 http://rednaxelafx.iteye.com/blog/492667
轉載請註明出處:http://blog.csdn.net/horkychen