執行代碼[WebKit] JavaScriptCore解析--基礎篇(二)解釋器基礎與JSC核心組件

http://www.cnblogs.com/jiangu66/archive/2013/05/15/3080571.html

之前一直在研究執行代碼之類的問題,現在正好有機會和大家討論一下.

    這一篇重要說明解釋器的基本工作過程和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


發佈了20 篇原創文章 · 獲贊 5 · 訪問量 15萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章