什么是字節(jié)碼舞虱?
字節(jié)碼(Byte-code)是一種包含執(zhí)行程序,由一序列 op 代碼/數(shù)據(jù)對(duì)組成的二進(jìn)制文件母市,是一種中間碼(IR)矾兜。是機(jī)器碼的一種抽象。
我們常說(shuō)的字節(jié)碼一般是Java字節(jié)碼患久,但其實(shí)很多動(dòng)態(tài)編譯解釋的語(yǔ)言都有字節(jié)碼椅寺,比如Javascript, python、ruby蒋失。
那么返帕,字節(jié)碼長(zhǎng)什么樣?我們用文本編輯器打開(kāi)對(duì)應(yīng)的文件篙挽,可以看到里面都是些二進(jìn)制的字符
下圖是V8的JS源碼荆萤、字節(jié)碼和機(jī)器碼
Hermes的JS源碼:
print("helloword");
Hermes的字節(jié)碼
Bytecode File Information:
Bytecode version number: 83
Source hash: 0000000000000000000000000000000000000000
Function count: 1
String count: 3
String Kind Entry count: 2
RegExp count: 0
Segment ID: 0
CommonJS module count: 0
CommonJS module count (static): 0
Bytecode options:
staticBuiltins: 0
cjsModulesStaticallyResolved: 0
Global String Table:
s0[ASCII, 0..5]: global
s1[ASCII, 6..14]: helloword
i2[ASCII, 15..19] #A689F65B: print
Function<global>(1 params, 11 registers, 0 symbols):
Offset in debug table: source 0x0000, lexical 0x0000
GetGlobalObject r0
TryGetById r2, r0, 1, "print"
LoadConstUndefined r1
LoadConstString r0, "helloword"
Call2 r0, r2, r1, r0
Ret r0
Debug filename table:
0: /tmp/hermes-input.js
Debug file table:
source table offset 0x0000: filename id 0
Debug source table:
0x0000 function idx 0, starts at line 1 col 1
bc 2: line 1 col 1
bc 14: line 1 col 6
0x000a end of debug source table
Debug lexical table:
0x0000 lexical parent: none, variable count: 0
0x0002 end of debug lexical table
為什么需要字節(jié)碼?
首先,我們知道JS是一種腳本語(yǔ)言链韭,他運(yùn)行在不同的平臺(tái)偏竟,包括Windows、Linux敞峭、MacOS踊谋、iOS、Android 等旋讹。不管什么樣的語(yǔ)言殖蚕,最終都是要變成機(jī)器碼的,而不同平臺(tái)由于有不同的處理器架構(gòu)以及對(duì)應(yīng)的指令集沉迹,所以就需要有一個(gè)中間層來(lái)負(fù)責(zé)對(duì)應(yīng)平臺(tái)的指令轉(zhuǎn)換成對(duì)應(yīng)的機(jī)器碼嫌褪,使得腳本語(yǔ)言的開(kāi)發(fā)者無(wú)需關(guān)心底層這些復(fù)雜的硬件和指令系統(tǒng)。這個(gè)中間層就是我們說(shuō)的虛擬機(jī)胚股,而在虛擬機(jī)上面執(zhí)行的代碼就是字節(jié)碼。
虛擬機(jī)是一種的抽象化的計(jì)算機(jī)裙秋,是通過(guò)在實(shí)際的計(jì)算機(jī)上仿真模擬各種計(jì)算機(jī)功能來(lái)實(shí)現(xiàn)的琅拌。虛擬機(jī)有自己完善的硬體架構(gòu),如處理器摘刑、堆棧进宝、寄存器等,還具有相應(yīng)的指令系統(tǒng)枷恕。虛擬機(jī)屏蔽了與具體操作系統(tǒng)平臺(tái)相關(guān)的信息党晋,使得程序只需生成在虛擬機(jī)上運(yùn)行的目標(biāo)代碼(字節(jié)碼),就可以在多種平臺(tái)上不加修改地運(yùn)行徐块。
例如JavascriptCore內(nèi)部的SquirrelFish未玻、V8的Ignition,以及React Native專用的Hermes胡控。
以V8為例扳剿,現(xiàn)在帶有字節(jié)碼的JS引擎工作流程一般如下:
但事實(shí)上,V8早期時(shí)沒(méi)有使用字節(jié)碼昼激,而是直接從JS轉(zhuǎn)換成機(jī)器碼
這樣執(zhí)行的性能確實(shí)比從JS轉(zhuǎn)成字節(jié)碼再轉(zhuǎn)成機(jī)器碼要更好庇绽,但后面他們發(fā)現(xiàn)這樣做有一些弊端:
機(jī)器碼占空間很大。在V8執(zhí)行的過(guò)程會(huì)將js源代碼轉(zhuǎn)化成二進(jìn)制代碼并且將二進(jìn)制代碼存儲(chǔ)到內(nèi)存中橙困,退出進(jìn)程后會(huì)將二進(jìn)制代碼存儲(chǔ)到硬盤(pán)上瞧掺。將js源碼轉(zhuǎn)化成的二進(jìn)制代碼占用的內(nèi)存空間是非常巨大的,如果說(shuō)一個(gè)js源碼的文件大小是1M凡傅,那么生成的二進(jìn)制代碼可能就是十幾M辟狈,而早期手機(jī)的內(nèi)存普遍不高,過(guò)度占用會(huì)導(dǎo)致性能大大降低夏跷。
-
代碼復(fù)雜度太高上陕。上文提到過(guò)不同的CPU架構(gòu)對(duì)應(yīng)的指令集是完全不同的桩砰,而市面上CPU架構(gòu)的種類又非常多,那么將AST轉(zhuǎn)化為二進(jìn)制代碼的Full-Codegen引擎以及優(yōu)化編譯的Crankshaft引擎要針對(duì)不同的CPU架構(gòu)編寫(xiě)代碼释簿,這個(gè)復(fù)雜程度及工作量可想而知亚隅,而對(duì)字節(jié)碼進(jìn)行編譯可以大大的減少這個(gè)工作量
v8.png
-
重復(fù)編譯bug
Bug的報(bào)告人在當(dāng)時(shí)的Chrome瀏覽器下重復(fù)加載Facebook,并打開(kāi)了各項(xiàng)監(jiān)控發(fā)現(xiàn):第一次加載時(shí) v8.CompileScript 花費(fèi)了 165 ms庶溶,而重復(fù)加載時(shí)發(fā)現(xiàn)真正耗時(shí)高的js代碼并沒(méi)有被緩存煮纵,導(dǎo)致重復(fù)加載時(shí)編譯的時(shí)間和第一次加載的消耗大致相同。
導(dǎo)致這個(gè)bug的原因其實(shí)也很好理解偏螺,之前提到過(guò)因?yàn)槎M(jìn)制代碼占用內(nèi)存空間大行疏,根據(jù)惰性編譯的優(yōu)化原則,所以V8并不會(huì)將所有代碼進(jìn)行編譯只會(huì)編譯最外層的代碼套像,而在函數(shù)內(nèi)部的代碼會(huì)在第一次調(diào)用時(shí)編譯酿联,比如:
如果瀏覽器只緩存最外層代碼,那么對(duì)我們前端高度工程化的模塊來(lái)說(shuō)會(huì)導(dǎo)致里面的關(guān)鍵代碼卻無(wú)法被緩存夺巩,這也是導(dǎo)致上述問(wèn)題的主要原因贞让。
而引入字節(jié)碼之后,上面的三個(gè)問(wèn)題就可以得到緩解柳譬。通過(guò)恰當(dāng)?shù)卦O(shè)計(jì)字節(jié)碼的編碼方式喳张,字節(jié)碼可以做到比機(jī)器碼緊湊很多。
啟動(dòng)時(shí)只需要編譯出字節(jié)碼美澳,然后逐句執(zhí)行字節(jié)碼销部,編譯出字節(jié)碼的速度可遠(yuǎn)遠(yuǎn)快于編譯出二進(jìn)制代碼的速度。
字節(jié)碼的形式
先看一段JS代碼
function incrementX(obj) {
return 1 + obj.x;
}
incrementX({x: 42});
V8字節(jié)碼
編譯為V8字節(jié)碼是這樣的
$ node --print-bytecode incrementX.js
...
[generating bytecode for function: incrementX]
Parameter count 2
Frame size 8
12 E> 0x2ddf8802cf6e @ StackCheck
19 S> 0x2ddf8802cf6f @ LdaSmi [1]
0x2ddf8802cf71 @ Star r0
34 E> 0x2ddf8802cf73 @ LdaNamedProperty a0, [0], [4]
28 E> 0x2ddf8802cf77 @ Add r0, [6]
36 S> 0x2ddf8802cf7a @ Return
Constant pool (size = 1)
0x2ddf8802cf21: [FixedArray] in OldSpace
- map = 0x2ddfb2d02309 <Map(HOLEY_ELEMENTS)>
- length: 1
0: 0x2ddf8db91611 <String[1]: x>
Handler Table (size = 16)
我們稍微解釋下其中一些指令的意思
LdaSmi [1]
LdaSmi [1] 將常量 1 加載到累加器中制跟。
Star r0
接下來(lái)舅桩,Star r0 將當(dāng)前在累加器中的值 1 存儲(chǔ)在寄存器 r0 中。
LdaNamedProperty a0, [0], [4]
LdaNamedProperty 將 a0 的命名屬性加載到累加器中雨膨。ai 指向 incrementX() 的第 i 個(gè)參數(shù)江咳。在這個(gè)例子中,我們?cè)?a0 上查找一個(gè)命名屬性哥放,這是 incrementX() 的第一個(gè)參數(shù)歼指。該屬性名由常量 0 確定。LdaNamedProperty 使用 0 在單獨(dú)的表中查找名稱:
- length: 1
0: 0x2ddf8db91611 <String[1]: x>
可以看到甥雕,0 映射到了 x踩身。因此這行字節(jié)碼的意思是加載 obj.x。
那么值為 4 的操作數(shù)是干什么的呢社露? 它是函數(shù) incrementX() 的反饋向量的索引挟阻。反饋向量包含用于性能優(yōu)化的 runtime 信息。
現(xiàn)在寄存器看起來(lái)是這樣的:
Add r0, [6]
最后一條指令將 r0 加到累加器,結(jié)果是 43附鸽。 6 是反饋向量的另一個(gè)索引脱拼。
Return
Return 返回累加器中的值。返回語(yǔ)句是函數(shù) incrementX() 的結(jié)束坷备。此時(shí) incrementX() 的調(diào)用者可以在累加器中獲得值 43熄浓,并可以進(jìn)一步處理此值。
乍一看省撑,V8 的字節(jié)碼看起來(lái)非常奇怪赌蔑,特別是當(dāng)我們打印出所有的額外信息。但是一旦你知道 Ignition 是一個(gè)帶有累加器寄存器的寄存器竟秫,你就可以分析出大多數(shù)字節(jié)碼都干了什么娃惯。
Hermes字節(jié)碼
同樣一段代碼,用Hermes生產(chǎn)的字節(jié)碼如下(導(dǎo)出成了可閱讀格式):
Bytecode File Information:
Bytecode version number: 83
Source hash: 0000000000000000000000000000000000000000
Function count: 2
String count: 3
String Kind Entry count: 2
RegExp count: 0
Segment ID: 0
CommonJS module count: 0
CommonJS module count (static): 0
Bytecode options:
staticBuiltins: 0
cjsModulesStaticallyResolved: 0
Global String Table:
s0[ASCII, 0..5]: global
i1[ASCII, 6..15] #FC148982: incrementX
i2[ASCII, 16..16] #0001E7F9: x
Function<global>(1 params, 11 registers, 0 symbols):
Offset in debug table: source 0x0000, lexical 0x0000
DeclareGlobalVar "incrementX"
CreateEnvironment r0
CreateClosure r1, r0, 1
GetGlobalObject r0
PutById r0, r1, 1, "incrementX"
GetByIdShort r2, r0, 1, "incrementX"
NewObject r1
LoadConstUInt8 r0, 42
PutNewOwnByIdShort r1, r0, "x"
LoadConstUndefined r0
Call2 r0, r2, r0, r1
Ret r0
Function<incrementX>(2 params, 2 registers, 0 symbols):
Offset in debug table: source 0x0010, lexical 0x0000
LoadParam r0, 1
GetByIdShort r1, r0, 1, "x"
LoadConstUInt8 r0, 1
Add r0, r0, r1
Ret r0
Debug filename table:
0: /tmp/hermes-input.js
Debug file table:
source table offset 0x0000: filename id 0
Debug source table:
0x0000 function idx 0, starts at line 1 col 1
bc 14: line 1 col 1
bc 20: line 5 col 1
bc 30: line 5 col 12
bc 36: line 5 col 11
0x0010 function idx 1, starts at line 1 col 1
bc 3: line 2 col 17
bc 11: line 2 col 10
0x001a end of debug source table
Debug lexical table:
0x0000 lexical parent: none, variable count: 0
0x0002 end of debug lexical table
Hermes字節(jié)碼的可讀版本可以通過(guò)https://hermesengine.dev/playground/ 或者 hermes test1.js -O -dump-bytecode顯示出來(lái)(這里test1.js 的內(nèi)容就是上面的JS代碼)
同樣的肥败,這里節(jié)選incrementX 這個(gè)函數(shù)里面的指令說(shuō)明下Hermes的字節(jié)碼指令:
LoadParam r0, 1
讀取第1個(gè)參數(shù)(也就是obj)趾浅,保存到r0寄存器。
GetByIdShort r1, r0, 1, "x"
從r0(obj)中讀取"x"屬性馒稍,保存到r1寄存器皿哨。這里第3個(gè)參數(shù)1 是用來(lái)加速的緩存索引。
LoadConstUInt8 r0, 1
讀取整數(shù)1筷黔, 保存到r0。 由于上一步已經(jīng)使用過(guò)r0了仗颈,所以r0的值沒(méi)有再保存的必要佛舱,這里重新給他賦值為整數(shù)1。
Add r0, r0, r1
把r0+r1的值寫(xiě)入r0挨决,也就是obj.x + 1
Ret r0
返回r0的值
對(duì)比V8和Hermes的指令请祖,我們可以看到不同的JS引擎生成的指令作用是類似的,只是指令的內(nèi)容和順序不太一樣脖祈。相比而言肆捕,V8多了一個(gè)累加器,專門(mén)用于做數(shù)字運(yùn)算盖高,對(duì)于屬性名的獲取更偏向于使用索引而不是直接用字符串(不確定是不是導(dǎo)出成可閱讀格式時(shí)填充上去的)慎陵,操作數(shù)也是能少則少。這樣做無(wú)疑字節(jié)碼會(huì)更精簡(jiǎn)喻奥,但對(duì)于閱讀來(lái)說(shuō)就比較麻煩了席纽。
參考鏈接
https://juejin.cn/post/6844904152745639949