內容來源:2017年6月24日,線上回聲公司前端技術專家迷渡在“騰訊Web前端大會 TFC 2017 ”進行《面向前端開發者的V8效能優化》演講分享。IT大咖說作為獨家視訊合作方,經主辦方和講者審閱授權釋出。
閱讀字數:2683 | 4分鐘閱讀
摘要
V8是一個由丹麥Google使用C++開發的開源JavaScript引擎,用於Google Chrome中,目前該JavaScript引擎已用於其它專案的開發。
嘉賓演講視訊地址:t.cn/RC0fGOl
PPT地址:t.cn/RpjFdlm
在V8中的數字表示
在V8中數字有小整數(SMI)和引用型別,它們是通過標記位進行表示的,以提升效能。
例如把整數42編碼為0×0000002a00000000;指標0×12345678編碼為0×12345679,非整數數值存放在堆裡。
這個例子是為了說明基於標記位的儲存方式,在 V8 引擎的內部並不是這麼儲存的。
在V8程式碼中使用C++的位運算去做比較,是為了提升V8引擎本身的效能。
如圖我做了一個基準測試。左邊的程式碼是V8單元測試中的程式碼,可見在32位中使用的是i30,在64位系統上,V8則會使用i31。
效能測試是基於64位進行的,通過效能測試會發現,前面的速度都非常快,到了i31再往上加的時候,速度就成倍的下降。
這套程式碼是js的另一段單元測試,測試的是js的程式碼。當我們不知道一個API如何使用或不知道一個東西內部是怎樣的時候,去看它的單元測試,就很容易知道它外部表現出來的是什麼樣,我們該如何去用。
在V8的class裡,它們都繼承了一個Value。Value分為Object和Primitive。Primitive下面是number,在number下面又會分成Int32和Uint32。
Object下面的分類很多,比如陣列函式,這些基本上都是Object型別。
Javascript中的“加法”
分析完資料型別,再來看看它的運算。在運算中經常會遇到一些問題,例如:
為什麼++[[]][+[]]+[+[]]=10?
{}+{}等於多少?
為什麼[1,2]+[3,4]不等於[1,2,3,4]?
在js的加法運算中,它有自己型別轉換的規則。js是一種弱型別,如果用不同型別去做加法,它會直接編譯器報錯。弱型別不是因為它沒有型別,只是它不像靜態語言那樣進行強制性轉換,而是有預設的規則進行轉換。
V8的算數運算
V8算數運算的快速模式就是直接呼叫二進位制程式碼assembly,包括小整數、堆區的數值,還有一些怪異的型別undefined、null、true、false,以及字串。
物件運算使用C++實現比較慢。
快速模式
編譯一段程式碼a + b,先把a放到一個暫存器,再把b放到一個暫存器,然後調一個函式,這個函式可以將a和b相加,相加結果會放到記憶體裡。這是常規的編譯方法。
要讓編譯的速度變快,進行優化編譯。把a和b放入暫存器,直接呼叫CPU指令add,然後將兩個暫存器相加,結果放進eax。但假如a和b是字串,就不能直接進行優化編譯。
Type feedback
V8引入了型別反饋技術。當我們進行二元運算的時候,V8會對所有運算的引數進行型別反饋,型別反饋給V8引擎。
這就是V8使用的優化編輯器。使用型別反饋做動態檢查,一般而言會在編譯階段提前檢查。檢查之後,使用該型別作為動態型別。如果檢查失敗,去優化(deopt)。去優化之後,可能會使用直譯器執行中間碼。
去優化Deoptimization
去優化就是生成一個未優化的幀,運算時,V8會把優化的幀去掉,呼叫的時候V8再重新進行優化。
當去優化並再次優化完成之後,最終會生成重新優化過的機器碼。如果要進行別的操作,V8還會進行優化操作。
要避免“去優化”
去優化的消耗大,主要是因為重新優化的消耗非常大。
如果我們不恰當的使用型別反饋資訊,那麼我們就會陷入去優化的怪圈:函式不停地去優化,然後再重新優化,直到我們達到了重優化的次數限制,這時我們的函式將再也不會被V8引擎優化。
Can we do better?
現在在很多庫裡,它們直接使用位運算和asm.js。
在位運算中,只對低32位有效。這是一個非常重要的型別反饋資訊。
截斷
在( x + y )|0運算時,我們只關心低32位的結果。即使x,y都是int52,我們也只關心x和y的低32位。
表示式+a[i] 不區分a[i]=undefined和a[i]=NaN。在稀疏陣列中,我們會讀取到NaN!而不是undefined。
表示式c ? x : y也不需要區分c=1和c=true。
“截斷”的其它用途
截斷還可以用於其他優化:
從double到integer轉換時的負零檢查;
乘法運算的負零檢查;
讀取陣列元素時的undefined檢查;
使引擎能更精準地表示型別。
截斷傳播只在V8的Turbofan編譯器有效。
面臨的挑戰
目前,引擎首先進行截斷分析,而型別反饋不影響截斷。
例如,( x + y|0 )中x和y將會被作為整型。理想情況下,使用x和y的型別反饋,然後進行int32加法。然而,很多情況下,最明智的選擇往往是“更差”的表示法。比如a + b + 0.5應該是float64,即使a,b被反饋為整型。
未來方向
JavaScript可以使用任意的精確的整數。我們可以更加精準的控制V8引擎生成的程式碼,也許以後會有(U)Int64或BigNum型別。
在未來的方向上有TypedArray、WebAssembly和SIMD三種。
TypedArray目前已經有了並被很多引擎和瀏覽器實現。
WebAssembly:我們可以用C++寫js程式碼,寫完直接生成抽象語法樹,讓V8進行進一步編譯。
SIMD充分發揮了CPU的優勢,單指令多運算並行。
V8 Binding:JS Object和DOM物件
如上圖,右邊是DOM樹。div下面有一個段落,段落有兩個子元素。右邊與之對應的,div會生成一個div的js object,p會生成p object。
TurboFan
TurboFan是V8即將使用的新引擎。
TurboFan IR是一個內部表示。當我們寫了一串程式碼,V8引擎對程式碼進行內部表示,最終才會進行優化操作,翻譯成我們所需要的程式碼。TurboFan所有的表示、優化都是基於圖。
V8速度快的原因就是內部使用了Hidden Classes,能直接把程式碼編譯成機器碼,效能非常高。
整數相加
首先我們建立一個add,傳了一個物件,依靠物件的兩個屬性(其實是一個屬性)進行相加。一個屬性表示它的型別相同。然後進行迴圈、相加。
我們用d8分析它的效能,如果沒有 d8 我們可以使用 ndoe.js 代替。圖上第一行進行了優化,並且寫了原因small function。因為函式非常小,V8對它進行了內聯操作。
混合相加
混合相加和整數相加的區別就是在於,我們生成0-1的隨機數,用0.5進行判斷。
最後幾行顯示,本來想優化,最後發現不能優化,因為沒有足夠的型別資訊。
圖中最長的一行程式碼經過優化後,下面的程式碼又不能優化了,要想繼續優化還要等待型別資訊。
--trace-depot
V8內部進行了去優化。
full gc
上面的程式碼不變,下面的程式碼在陣列裡放了一個很大的物件,有5%的概率將這個物件釋放。
--allowe-natives-syntax
當我們在除錯js效能或寫一些效能要求很高的庫的時候,會經常使用到這個語法。它允許我們在js程式碼裡使用C++函式。
這是程式碼生效後的結果。
Bluebird promise
Bluebird是用在promise的一個庫,這是我經常使用的一個庫。在很多場景下它比原生的用得還要高,因為它能加快object的訪問速度。
累加
我們進行一個累加的遞迴。比如sum操作,如果是1返回1,不是1則返回當前數和之前數的累加。
下圖是它的呼叫過程。
呼叫棧
每次呼叫函式要開闢一個棧,當再呼叫的時候,從這個函式裡又開闢出了一個新的棧然後返回。最後返回我們的值。
如果我們使用的是尾呼叫(函式的最末尾呼叫了另一個函式),其實我們不用開闢新的棧,只需要使用同一個棧去做所有的操作都行。因為即使開闢了新棧,當前棧也不再使用了。這對記憶體的保護有很大的優化作用。
如圖可見,優化後中間有一個棧的呼叫丟失了。
現在的解決方式就是我們可以進行顯式指定,有三個待選的語法return continue、!return、#function()。
我今天的分享就到這裡,感謝聆聽!