javascript
[WebKit] JavaScriptCore解析
- Chakra(Microsoft Internet Explorer)
- Nitro/JavaScript Core?(Safari)
- Carakan?(Opera)
- SpiderMonkey?(Firefox)
- V8?(Chrome, Chromium)
先看一下官方的基本介紹,短短幾句就塞滿了關鍵字。
SquirrelFish,正式名稱是JavaScriptCore,包括register-based(基于寄存器的虛擬機), direct-threaded, high-level bytecode engine(字節碼引擎).它使用基于內置copy propagation(復制性傳播算法)的一次性編譯器(one-pass compiler),能夠延遲從語法樹(Syntax Tree)上生成字節碼(Bytecodes)。
由此可見JavaScriptCore實現的復雜度。做為一個正在努力學習的菜鳥,我愿意給自己這樣一個挑戰,通過記錄和總結學習內容,分成兩個大的段落從內部視角來解析JavaScriptCore。首先是基礎篇,目的是了解JavaScriptCore是如何與WebKit一起工作的,會涉及一些JavaScript引擎的一些基本概念。然后是高級篇,嘗試解釋JavaScriptCore中的核心技術,如Byte Code Compiler, JIT, VM以及GC等。
內容或許會顯得晦澀,如果只是想簡單地了解瀏覽器JS引擎的一些基本內容,推薦讀讀下面的文章。它們對于理解后面的內容也會非常有幫助。相對于這些資料,這個系列則側重于從基礎及從實例來分析JSC的實現。沒辦法做到高層次,只能追求貼近實現。
1、JavaScriptCore, WebKit的JS實現(一)
2、JavaScriptCore, WebKit的JS實現(完)
3、為什么V8引擎這么快?
當然,JavaScript的知識是必不可少的,推薦閱讀一篇(JavaScript核心指南),如果有時間可以深入學習一下其中提到的鏈接。
一. JavaScriptCore與WebCore
兩者的關系可以簡單的用下圖來表示:
JSC為WebCore提供兩個重要功能:
1. JS腳本的解析執行 (ScriptController)
主要是通過調用JavaScriptCore提供的兩個C接口來實現的, checkSyntax和evaluate.
2. DOM節點的JS Bindings
DOM節點所對應的JS Bindings都可以回溯到JSC::JSNonFinalObject,再到JSObject,以實現和JSC綁定在一起。
*關于JS Binding,可以先看一下這篇文章: 為JavaScript Binding添加新DOM對象的三種方式及實作。至于JSC實現的細節,以后再展開。
二. JavaScriptCore基本工作過程
JSC最簡單的執行過程如下,再如之后JIT等在這個基礎上的優化。
三. JavaScript腳本的執行
以下分層說明腳本執行的步驟。 對于涉及到編譯及執行的細節,則在后續解釋。
3.1 接口層的交互
JSC和其它幾個主要的JS Engine一樣,都是一個庫,通過提供簡單的API來供調用者使用。
從JSC接口來看,一個完整的JavaScript腳本的解析執行過程,可以概述以下:
過程很簡單,可是很明顯有些關鍵詞必須要理解一下,如VM, Global Object, ExecState. 它們的關系也可以通過一張圖來解釋:
VM -> Virtual Machine, JavaScript要借助于一個運行時(Runtime)環境來運行。 SpiderMonkey就稱之為Runtime.
GlobalObject -> 腳本執行時的全局對象。一個全局負責組織管理執行環境以及各個子對象。
ExecState -> 用于記錄腳本執行上下文或環境, 也由GlobalObject管理。SpiderMoney以及Apple封裝后的JavaScriptCore.framework都稱之為上下文(Context). 可以將其視為一個執行腳本的對象來理解,只是它所產生和使用的Objects是共享的,并可以由GlobalObject來訪問。
JS解釋器各自實現的方式略有不同,JSC是由一個全局變量(Global Object)來創建上下文環境(ExecState), 而SpiderMonkey則是由執行上下文來創建全局變量。但無論哪種實現,全局變量和上下文都一一對應的,雖然原則上是允許一對多的情況出現。
最后看下JSC執行JS腳本的接口定義, 就很好理解了:
JSValue evaluate(ExecState* exec,constSourceCode& source,JSValue thisValue,JSValue* returnedException);
*thisValue就是JavaScript的this, 代表的是執行者, 但不一定是創建者。
*使用JSC的示例代碼,可以看看WebKit里的jsc.cpp就可以了。
*如果覺得沒有講清楚,建議讀讀這里(JavaScript核心指南)。
3.2 JSC API執行腳本的步驟
下圖是JSC API函數evaluate的活動圖:
重點在于它會使用要執行的腳本內容建立一個ProgramExecutable對象,然后調用Interpreter執行這個代表腳本的ProgramExecutable對象。
ProgramExecutable和Interpreter都是JSC核心類,ProgramExecutable負責編譯代碼為ByteCode,屬于解釋器功能組, 而Interpreter則負責解析執行ByteCode,則屬于VM功能組.
*Interpreter提供的兩個dump函數對于分析代碼也很有用, dumpCallFrame和dumpRegisters。
四. DOM Bindings的響應
實現上的解析在這里:WebKit的JS Binding解析
以及另一篇可以加深理解:為JavaScript Binding添加新DOM對象的三種方式及實作
這一篇主要說明解釋器的基本工作過程和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快速入門。
執行的基礎環境(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輸出。
前面說了一些解析、生成ByteCode直至JIT的基本概念,下面是對照JavaScriptCore源代碼來大致了解它的實現。
從JS Script到Byte Code
首先說明Lexer, Parser和ByteCode的生成都是由ProgramExecutable初始化過程完成的。首先在JSC的API evaluate()中會創建ProgramExecutable并指定腳本代碼。然后傳入Interpreter時,再透過CodeCache獲取的UnlinkedProgramCodeBlock就是已經生成ByteCode后的Code Block了。
下圖是CodeCache調用Parser和ByteCodeGenerator的序列圖:
而Lexer則是在Parser過程中調用的,如下圖:
再從類圖來觀察所涉及的幾個類之間的關系:
關于CodeBlock、UnlinkedCodeBlock和ScriptExecutable
CodeBlock可以理解為代碼管理的類,按類型分為GlobalCodeBlock, ProgramCodeBlock, FunctionCodeBlock及EvalCodeBlock, 與之對應的UnlinkedCodeBlock和ScriptExecutable也有相似的繼承體系,如下所示:
UnlinkedCodeBlock存儲的是編譯后的ByteCode,而CodeBlock則會用于LLint和JIT。
ProgramExecutable則可以理解為當前所執行腳本的大總管,從其名字上可以看出來是代表一個可執行程序。
它們的作用也很容易理解。
關于LLint的slow path
前面說過了LLint是基于offlineasm的匯編語言,這里只是介紹一下它的slow path. 為了處理一些操作,需要在LLint執行指令時調用一些C函數進行擴展處理,比如后面要說明的JIT統計功能,LLint提供一個調用C函數的接口,并將所有會被調用的C函數稱為slow path,如下圖所示:
代碼可以在LowLevelInterpreterXXX.asm中看到。所以可以C函數聲明看到帶有SLOW_PATH的宏。
關于JIT優化的觸發
首先JSC使用的是基于計數器的熱點探測方法。前面提到函數或循環體被執行若干次后會觸發JIT, 首先這個次數是可以通過JSC::Options中的thresholdForOptimizeSoon來設定的。然后在LLint在執行循環的ByteCode指令loop_hint和函數返回指令ret時會調用slow path中的C函數,進行次數統計和判斷,過程如下:
其中會根據checkIfJITThresholdReached()返回結果來決定是否進行jitCompile.一旦要進行JIT編譯時,也是根據當前CodeBlock的類型,而執行針對不同函數或代碼段的優化。下面顯示的是對一個頻繁使用的函數進行JIT編譯的操作:
其中計數的功能并非由CodeBlock直接實現,而是通過ExecutionCounter來管理的。主要關系如下:
總結
以上是生活随笔為你收集整理的[WebKit] JavaScriptCore解析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 《python 源码剖析》 读后总结(虚
- 下一篇: iPhone怎么导入通讯录?