Js引擎解析執行 閱讀筆記

Js引擎解析執行 閱讀筆記


一篇閱讀筆記
http://km.oa.com/group/2178/articles/show/145691?kmref=search&from_page=1&no=1javascript

早期:遍歷語法樹

Js引擎最先使用的是遍歷語法樹方式
(syntax tree walker)java

分爲兩步python

  • 詞法分析
  • 語法分析

詞法分析

i = a + b * c;
轉換
"i", "=", "a", "+", "b", "*", "c";c++

語法分析

生成語法樹

 執行這條語句,就是遍歷這顆語法樹的過程。遍歷語法樹的過程在程序設計上通常採用訪問者模式(vistor pattern)來實現。要遍歷這顆語法樹,只要將根節點傳給visit函數, 而後這個函數遞歸調用相應子節點的visit函數,如此反覆直到葉子節點。例如,在這個例子中根節點是個賦值語句,他知道應該計算出右邊表達式的值,而後賦給左邊的地址;而在計算右邊表達式的時候,發現是一個加法表達式,因而接着遞歸計算加法表達式的值,如此遞歸進行直到這顆樹的葉子節點,而後一步步回溯,將值傳到到根節點,就完成了一次遍歷,也即完成了一次執行。
  要執行一棵語法樹,其實是一個後序遍歷樹的過程。以上面這個例子,要計算賦值語句,先計算加法表達式,那就必須先計算乘法表達式,也就是說只有子結點計算好了以後,父節點才能計算,典型的後序遍歷。
  web


中期:字節碼(bytecode)

在引擎的語境下,字節碼指的是虛擬機執行的中間指令集。
如:windows

  • Java編譯器把Java編譯成Java字節碼,而後在Java虛擬機中執行
  • ActiveScript,轉換成字節碼,在FLASH虛擬機中執行

分類數組

  • 基於棧stack-based
  • 基於寄存器register-based

若是在後序遍歷這棵樹後,生成對應的後綴記法(逆波蘭式)的操做序列,而後在執行時,直接解釋執行這後綴記法的操做序列。那麼就把這種樹狀結構,變換成了一種線性結構。這種操做序列就是字節碼(bytecode),這種執行方式就是字節碼解釋方式(bytecode interpreter)。框架

此處輸入圖片的描述
 
傳統的字節碼設計大可能是基於棧的,這種方式將全部的操做數和中間表示都保存在一個數據棧中。
如語句:c = a + b,轉換後的字節碼以下:函數

LOAD a  # 將a推入棧頂
LOAD b  # 將b推入棧頂
ADD     # 從棧頂彈出兩個操做數,相加後,將結果推入棧頂
STORE c  #將棧頂數據保存到C中

基於寄存器的字節碼經過寄存器(register)保存操做數。這裏與彙編代碼中的寄存器是兩個概念。寄存器能夠想象成是一個固定數組。上例轉換成基於寄存器的代碼以下:oop

ADD c, a, b   # 兩個操做數分別存在a和b中,將結果放在c中。

棧式字節碼每條的指令更短(目的地址不用顯式表示),可是總的指令條數更多。
棧式虛擬機實現比寄存器式簡單。
Flash Player的ActionScript虛擬機Tamarin、Firefox的JagerMonkey採用的是棧式設計;webkit,carakan採用寄存器方式。
字節碼是須要在虛擬機中執行的,而虛擬機的執行過程與CPU過程相似,也是取指,解碼,執行的過程。一般狀況下,每一個操做碼對應一段處理函數,而後經過一個無限循環加一個switch的方式進行分派。如:

switch loop

這裏的vpc是一個字節碼數組的指針,做用與PC寄存器相似,稱做虛擬PC(virtual program counter)。字節碼序列直接描述要執行的動做,去除語法信息;執行一條字節碼語句,只是一次的內存訪問(取指令)加上一次間接跳轉(分派處理函數),比訪問語法樹中節點的開銷要小。所以,字節碼方式與遍歷語法樹相比在性能上有很大的提高。雖然從語法樹生成字節碼須要時間,可是這一段時間能夠從直接執行字節碼所得到的性能提高上獲得補償。畢竟在實際的代碼中,不會全部的代碼都只被執行一次。並且生成了字節碼以後,就能夠對於這種中間代碼進行各類優化,好比常量傳播,常量摺疊,公共子表達式刪除等等。固然這些優化都是有針對性和選擇性的,畢竟優化的過程也是須要消耗時間的。而這些優化要想直接在語法樹上進行幾乎是不可能的。

Driect Threading

字節碼方式相對於遍歷語法樹已經前進了一大步,可是在分派方式上還能夠再改進。Switch Loop分派方式每次處理完一條指令後,都要回到循環的開始,處理下一條,而且每次switch操做,都是一次線性搜索(現代編譯器通常都能對switch語句進行優化, 以消除線性搜索開銷,但這種優化只限於特定條件,如case的數量和值的跨度範圍等),對於通常的函數,只有有限的幾個switch case,尚可接受,可是對於虛擬機來講,有上百個switch case而且頻繁地執行,執行一條指令就須要一次線性搜索,仍是太慢了。若是能用查表的方式直接跳轉,就能夠省去線性搜索的過程了。因而在字節碼的分派方式上,新的改進稱做Direct Threading。

Direct
Threading,這裏的threading與咱們一般理解的線程沒有任何關係,能夠理解成是針線中的那個「線」。以這種方式執行時,每執行完一條指令後不是回到循環的開始,而是直接跳到下一條要執行的指令地址。這種方式就比原來的Switch
Loop方式有效許多。可是要想有效的實現Direct Threading,須要用到一個gcc的擴展「Labels As
Values」,普通的goto語句的標號是在編譯時指定的,可是利用「Labels As
Values」擴展,goto語句的標號是就能夠在運行時計算(這種goto語句也叫Computed
Goto),利用這個特性就能夠很容易地實現Direct
Threading。(想在windows平臺用這個特性,也有幾個GCC的windows移植版本,如MinGW, Cygwin等)
右圖中的Direct Threading方式已經沒有了循環和switch分支,全部的字節碼分派就是經過「goto *vpc++」進行的。

在引入即時編譯(JIT)以前,Direct Threading方式是字節碼解釋器最有效和最塊的分派方式。對於通常的JavaScript運算,這種方式足夠用了。可是解釋執行方式確定比不上直接執行二進制代碼。因而接下來即時編譯(JIT)技術被引入了JavaScript引擎。


如今:即時編譯Just-In-Time

字節碼指令--->本地機器碼

JIT這種技術自己很古老,能夠追溯到60年代的LISP語言;現代的大部分運行時環境(runtime environment),如微軟的.NET框架和大多數的Java實現都是依賴JIT技術來提升性能。在JavaScript引擎中引入JIT是在2008年開始的。
JIT是一種提升性能的方法。一般一個程序有兩種方式執行:靜態編譯和解釋執行。靜態編譯就是在運行前先將源代碼(如c,c++)針對特定平臺(如x86,arm,mips)編譯成機器代碼,在運行時就能夠直接在相應的平臺上執行;
而解釋執行則是每次運行的時候,將每條源代碼(如python, javascript)翻譯成相應的機器碼並馬上執行,並不保存翻譯後的機器碼,周而復始。能夠看到解釋執行的運行效率很低,由於每次執行都須要逐句地翻譯成機器碼而後執行;而靜態編譯在運行前就編譯成相應平臺的代碼。可是靜態編譯使得平臺移植性不好,也沒法實施運行時優化,並且對於動態語言(弱類型語言),變量的類型在運行前未知,很難作到靜態編譯。JIT編譯則是這兩種方式的混合,在運行時將源代碼翻譯成機器碼(這一點與解釋執行相似),可是會保存已翻譯的機器代碼,下次執行同一代碼段時無需再翻譯(這又與靜態編譯相似)
在實際的實現中,對於簡單的指令,如mov,就直接即時編譯,inline到機器碼中;對於複雜的指令,如add指令,會對它的經常使用方式(如操做數是數值或字符串)直接生成對應的機器碼,對於add的其餘不經常使用狀況(如一個操做數是數值,另外一個是字符串)則是生成一條call本地調用
字節碼編譯成本地機器碼(JIT的過程)須要消耗執行時間,因此不是對全部代碼都會生成機器碼,而是隻對熱點(hot spot)片斷進行即時編譯,同時在運行中會隨時跟蹤熱點的狀態,若是熱點的程度越高(被執行得越頻繁),實施的優化也越激進。

此處輸入圖片的描述

以firefox爲例,在開始執行時,將源代碼生成字節碼,而後解釋執行字節碼,在執行過程當中,若是發現一條路徑屢次執行(好比一個循環體),那麼就標記爲「HOT」,同時將這條路徑上的代碼即時編譯成機器碼,當下次再運行到這條路徑時,就直接運行機器碼。 在上圖判斷熱點的虛框中,若是一個路徑被執行了超過16次(好比「循環」迭代了超過16次),或一個函數被調用超過16次,那麼就進行即時編譯;不然解釋執行。

相關文章
相關標籤/搜索