JS引擎與字節(jié)碼

什么是字節(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)制的字符

image-20220109210401562.png

下圖是V8的JS源碼荆萤、字節(jié)碼和機(jī)器碼

img

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引擎工作流程一般如下:

2017

但事實(shí)上,V8早期時(shí)沒(méi)有使用字節(jié)碼昼激,而是直接從JS轉(zhuǎn)換成機(jī)器碼

2010

這樣執(zhí)行的性能確實(shí)比從JS轉(zhuǎn)成字節(jié)碼再轉(zhuǎn)成機(jī)器碼要更好庇绽,但后面他們發(fā)現(xiàn)這樣做有一些弊端:

  1. 機(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)致性能大大降低夏跷。

  2. 代碼復(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
  1. 重復(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í)編譯酿联,比如:


v8 function.png

如果瀏覽器只緩存最外層代碼,那么對(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ī)器碼緊湊很多。

memory.png

啟動(dòng)時(shí)只需要編譯出字節(jié)碼美澳,然后逐句執(zhí)行字節(jié)碼销部,編譯出字節(jié)碼的速度可遠(yuǎn)遠(yuǎn)快于編譯出二進(jìn)制代碼的速度。


pageload.png

字節(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 加載到累加器中制跟。

img

Star r0

接下來(lái)舅桩,Star r0 將當(dāng)前在累加器中的值 1 存儲(chǔ)在寄存器 r0 中。

img

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)是這樣的:

img

Add r0, [6]

最后一條指令將 r0 加到累加器,結(jié)果是 43附鸽。 6 是反饋向量的另一個(gè)索引脱拼。

img

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

https://zhuanlan.zhihu.com/p/28590489

https://hermesengine.dev/docs/design

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市撞蚕,隨后出現(xiàn)的幾起案子润梯,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,183評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件纺铭,死亡現(xiàn)場(chǎng)離奇詭異寇钉,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)舶赔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)扫倡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人顿痪,你說(shuō)我怎么就攤上這事镊辕。” “怎么了蚁袭?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,766評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵征懈,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我揩悄,道長(zhǎng)卖哎,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,854評(píng)論 1 299
  • 正文 為了忘掉前任删性,我火速辦了婚禮亏娜,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘蹬挺。我一直安慰自己维贺,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,871評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布巴帮。 她就那樣靜靜地躺著溯泣,像睡著了一般。 火紅的嫁衣襯著肌膚如雪榕茧。 梳的紋絲不亂的頭發(fā)上垃沦,一...
    開(kāi)封第一講書(shū)人閱讀 52,457評(píng)論 1 311
  • 那天,我揣著相機(jī)與錄音用押,去河邊找鬼肢簿。 笑死,一個(gè)胖子當(dāng)著我的面吹牛蜻拨,可吹牛的內(nèi)容都是我干的池充。 我是一名探鬼主播,決...
    沈念sama閱讀 40,999評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼缎讼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼纵菌!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起休涤,我...
    開(kāi)封第一講書(shū)人閱讀 39,914評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤咱圆,失蹤者是張志新(化名)和其女友劉穎笛辟,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體序苏,經(jīng)...
    沈念sama閱讀 46,465評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡手幢,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,543評(píng)論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了忱详。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片围来。...
    茶點(diǎn)故事閱讀 40,675評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖匈睁,靈堂內(nèi)的尸體忽然破棺而出监透,到底是詐尸還是另有隱情,我是刑警寧澤航唆,帶...
    沈念sama閱讀 36,354評(píng)論 5 351
  • 正文 年R本政府宣布胀蛮,位于F島的核電站,受9級(jí)特大地震影響糯钙,放射性物質(zhì)發(fā)生泄漏粪狼。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,029評(píng)論 3 335
  • 文/蒙蒙 一任岸、第九天 我趴在偏房一處隱蔽的房頂上張望再榄。 院中可真熱鬧,春花似錦享潜、人聲如沸困鸥。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,514評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)疾就。三九已至,卻和暖如春吕座,著一層夾襖步出監(jiān)牢的瞬間虐译,已是汗流浹背瘪板。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,616評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工吴趴, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人侮攀。 一個(gè)月前我還...
    沈念sama閱讀 49,091評(píng)論 3 378
  • 正文 我出身青樓锣枝,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親兰英。 傳聞我的和親對(duì)象是個(gè)殘疾皇子撇叁,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,685評(píng)論 2 360

推薦閱讀更多精彩內(nèi)容