前言
變數宣告在程式語言裡面是再基本不過的功能,同時也是一個非常常用的功能。因此,從宣告變數以及賦值來看 bytecode,應該是個很不錯的切入點。
在進入到 bytecode 之前,有個相當重要的基礎知識,那就是 V8 有一個叫做 acuumulator 的 register(以下簡稱 acc),有些指令的參數只會有一個,就是代表著把某個值放到 acc,或是從 acc 拿出來。
例如說指令:LdaSmi [1]
,就是 Load small integer into accumulator 的意思,而後面那個[1]
就是要載入的值。所以執行完這一行以後,acc 的值就會變成 1。
而像是 Star r0
這種指令,就代表著把目前 acc 的值存到目的地去。因此 Star r0
就是 r0 = acc
的意思。
這個 acc 的概念相當重要,有了這一個基礎知識以後,才能看懂之後 bytecode 想表達的意思。
宣告變數
先來試試看以下程式碼:
function find_me_test() {
var a = 1
let b = 2
const c = 3
}
find_me_test()
產生出來的 bytecode 如下:
[generated bytecode for function: find_me_test]
Parameter count 1
Frame size 24
21 E> 0x29da1b21dcba @ 0 : a5 StackCheck
36 S> 0x29da1b21dcbb @ 1 : 0c 01 LdaSmi [1]
0x29da1b21dcbd @ 3 : 26 fb Star r0
48 S> 0x29da1b21dcbf @ 5 : 0c 02 LdaSmi [2]
0x29da1b21dcc1 @ 7 : 26 fa Star r1
62 S> 0x29da1b21dcc3 @ 9 : 0c 03 LdaSmi [3]
0x29da1b21dcc5 @ 11 : 26 f9 Star r2
0x29da1b21dcc7 @ 13 : 0d LdaUndefined
64 S> 0x29da1b21dcc8 @ 14 : a9 Return
Constant pool (size = 0)
Handler Table (size = 0)
為了方便觀看,我們可以把前面的資訊都拿掉,只留下程式碼:
StackCheck
LdaSmi [1]
Star r0
LdaSmi [2]
Star r1
LdaSmi [3]
Star r2
LdaUndefined
Return
StackCheck
是一定會有的指令,是用來檢查 stack 有沒有 overflow 用的。LdaSmi [1]
前面說過了,就是 acc = 1
,然後Star r0
會把這個值存到 r0 去,而底下的幾行程式碼也類似。
因此可以推測出 r0 就是變數 a,r1 就是 b,r2 就是 c。
最後面兩行的意思也很簡單,LdaUndefined
其實就是 acc = undefined
,然後 Return
沒有接任何參數,因為會直接把 acc 裡面的值傳回去。在 function 裡面如果沒有寫任何 return,預設就會回傳 undefined。
因此,可以把上面那一段 bytecode 翻成白話文,一行就是一個指令:
StackCheck // check stack
LdaSmi [1] // acc = 1
Star r0 // r0 = acc
LdaSmi [2] // acc = 2
Star r1 // r1 = acc
LdaSmi [3] // acc = 3
Star r2 // r2 = acc
LdaUndefined // acc = undefined
Return // return acc
基本的語法沒有問題之後,我們可以針對各個關鍵字進一步來研究,在var
、let
與const
之中,我們直接來看比較多東西可以研究的const
。
深入 const
如果 const 沒有給值的話會發生什麼事呢?
function find_me_test() {
const c
}
find_me_test()
產生出來的結果是:
a.js:2: SyntaxError: Missing initializer in const declaration
const c
^
SyntaxError: Missing initializer in const declaration
因為是 SyntaxError,所以在還沒翻譯成 bytecode 之前就知道有錯誤,因此是不會有 bytecode 的。
那如果換成重複賦值呢?
function find_me_test() {
const c = 1
c = 2
}
find_me_test()
結果:
[generated bytecode for function: find_me_test]
Parameter count 1
Frame size 8
21 E> 0x4f21349dc8a @ 0 : a5 StackCheck
38 S> 0x4f21349dc8b @ 1 : 0c 01 LdaSmi [1]
0x4f21349dc8d @ 3 : 26 fb Star r0
42 S> 0x4f21349dc8f @ 5 : 0c 02 LdaSmi [2]
44 E> 0x4f21349dc91 @ 7 : 61 41 01 fb 00 CallRuntime [ThrowConstAssignError], r0-r0
0x4f21349dc96 @ 12 : 0d LdaUndefined
48 S> 0x4f21349dc97 @ 13 : a9 Return
Constant pool (size = 0)
Handler Table (size = 0)
a.js:3: TypeError: Assignment to constant variable.
c = 2
^
TypeError: Assignment to constant variable.
at find_me_test (a.js:3:5)
at a.js:6:1
因為這是TypeError
,是運行時才會產生的錯誤,因此還是會有 bytecode 產生,而產生出來的程式碼多了一行:CallRuntime [ThrowConstAssignError], r0-r0
來拋出錯誤。
接著來看一下 TDZ 的部分,不清楚那是什麼的可以參考:我知道你懂 hoisting,可是你了解到多深?。
為了方便對照,先來一個正常不會有錯誤的版本:
function find_me_test() {
const a = 1
console.log(a)
}
find_me_test()
產生的 bytecode:
[generated bytecode for function: find_me_test]
Parameter count 1
Frame size 24
21 E> 0x3fce2f39dcaa @ 0 : a5 StackCheck
38 S> 0x3fce2f39dcab @ 1 : 0c 01 LdaSmi [1]
0x3fce2f39dcad @ 3 : 26 fb Star r0
42 S> 0x3fce2f39dcaf @ 5 : 13 00 00 LdaGlobal [0], [0]
0x3fce2f39dcb2 @ 8 : 26 f9 Star r2
50 E> 0x3fce2f39dcb4 @ 10 : 28 f9 01 02 LdaNamedProperty r2, [1], [2]
0x3fce2f39dcb8 @ 14 : 26 fa Star r1
50 E> 0x3fce2f39dcba @ 16 : 59 fa f9 fb 04 CallProperty1 r1, r2, r0, [4]
0x3fce2f39dcbf @ 21 : 0d LdaUndefined
57 S> 0x3fce2f39dcc0 @ 22 : a9 Return
Constant pool (size = 2)
0x3fce2f39dc31: [FixedArray] in OldSpace
- map: 0x3fce709807b1 <Map>
- length: 2
0: 0x3fcee30100e9 <String[#7]: console>
1: 0x3fcee300fbe9 <String[#3]: log>
Handler Table (size = 0)
1
a 一樣會存在 r0,至於console.log
則是這一段:
LdaGlobal [0], [0]
Star r2
LdaNamedProperty r2, [1], [2]
Star r1
CallProperty1 r1, r2, r0, [4]
我們一行一行來解釋:
第一行 LdaGlobal [0], [0]
,只要發現看不懂的語法,都可以去 src/interpreter/interpreter-generator.cc 找解釋:
// LdaGlobal <name_index> <slot>
//
// Load the global with name in constant pool entry <name_index> into the
// accumulator using FeedBackVector slot <slot> outside of a typeof.
IGNITION_HANDLER(LdaGlobal, InterpreterLoadGlobalAssembler) {
static const int kNameOperandIndex = 0;
static const int kSlotOperandIndex = 1;
LdaGlobal(kSlotOperandIndex, kNameOperandIndex, NOT_INSIDE_TYPEOF);
}
會載入 global 裡的東西進去 acc,至於是什麼東西呢?要看傳進去的第一個參數 <name_index>
並且去 constant pool 裡面找;至於第三個參數 FeedBackVector slot 可以先不管它。
constant pool 裡面 0 的位置是:0: 0x3fcee30100e9 <String[#7]: console>
,因此 LdaGlobal [0], [0]
其實就是:acc = global.console
的意思。
接著下一行 Star r2
,把 global.console
存進去 r2,再下一行:LdaNamedProperty r2, [1], [2]
一樣可以去查意思:
// LdaNamedProperty <object> <name_index> <slot>
//
// Calls the LoadIC at FeedBackVector slot <slot> for <object> and the name at
// constant pool entry <name_index>.
constant pool 1 的位置為:1: 0x3fcee300fbe9 <String[#3]: log>
,所以這一行其實就是:acc = r2.log
,也就是 acc = console.log
。
再來 Star r1
把這個存進 r1,最後呼叫 CallProperty1 r1, r2, r0, [4]
,這一行其實就是 console.log(r0)
,最後就把我們的變數 a 給印出來了。
然後我們把 log 跟宣告變數調換一下位置,先 log 再宣告變數:
function find_me_test() {
console.log(a)
const a = 1
}
find_me_test()
結果為:
[generated bytecode for function: find_me_test]
Parameter count 1
Frame size 24
0x2ba6d6e9dcb2 @ 0 : 0f LdaTheHole
0x2ba6d6e9dcb3 @ 1 : 26 fb Star r0
21 E> 0x2ba6d6e9dcb5 @ 3 : a5 StackCheck
28 S> 0x2ba6d6e9dcb6 @ 4 : 13 00 00 LdaGlobal [0], [0]
0x2ba6d6e9dcb9 @ 7 : 26 f9 Star r2
36 E> 0x2ba6d6e9dcbb @ 9 : 28 f9 01 02 LdaNamedProperty r2, [1], [2]
0x2ba6d6e9dcbf @ 13 : 26 fa Star r1
0x2ba6d6e9dcc1 @ 15 : 25 fb Ldar r0
40 E> 0x2ba6d6e9dcc3 @ 17 : aa 02 ThrowReferenceErrorIfHole [2]
36 E> 0x2ba6d6e9dcc5 @ 19 : 59 fa f9 fb 04 CallProperty1 r1, r2, r0, [4]
55 S> 0x2ba6d6e9dcca @ 24 : 0c 01 LdaSmi [1]
0x2ba6d6e9dccc @ 26 : 26 fb Star r0
0x2ba6d6e9dcce @ 28 : 0d LdaUndefined
57 S> 0x2ba6d6e9dccf @ 29 : a9 Return
Constant pool (size = 3)
0x2ba6d6e9dc31: [FixedArray] in OldSpace
- map: 0x2ba611e007b1 <Map>
- length: 3
0: 0x2ba6f46900e9 <String[#7]: console>
1: 0x2ba6f468fbe9 <String[#3]: log>
2: 0x2ba6d6e9d8e9 <String[#1]: a>
Handler Table (size = 0)
a.js:2: ReferenceError: a is not defined
console.log(a)
^
ReferenceError: a is not defined
at find_me_test (a.js:2:15)
at a.js:6:1
仔細觀察就會發現開頭多了這兩行:
LdaTheHole
Star r0
意思就是 r0 = hole
,那這個 hole 是什麼呢?直翻的話可以翻作就是一個「洞」,先讓變數 a 的內容等於一個洞。而這個洞的功用就是:「在被填滿之前,不允許任何人存取」。
所以呼叫 console.log
以前,則是多了這兩行:
Ldar r0
ThrowReferenceErrorIfHole [2]
先把 r0 的值載入到 acc,接著檢查有沒有洞:
// ThrowReferenceErrorIfHole <variable_name>
//
// Throws an exception if the value in the accumulator is TheHole.
因為 r0 還沒賦值,所以的確是一個洞,因此最後就拋出了 ReferenceError。以上這幾組 bytecode 的指令就是 V8 對於 TDZ 的實作。
宣告多個變數
前面宣告變數的時候,會依照順序把值存進 r0, r1...,我想知道這有沒有上限,例如說暫存器是不是用到 r256 之類的就結束了。
為了達成這個目的,可以寫一段程式碼自動來產生宣告變數的程式碼:
var s = 'abcdefghijklmnopqrstuvwxyz'
var n = () => s[Math.floor(Math.random()*s.length)]
for(var i=1; i<=1000; i++){
console.log('var ' + n() + n() + n() + n() + n() + ' = 1')
}
接著我們把產生出來的程式碼放到 function 裡面產生 bytecode,可以得到以下結果(只擷取部分):
21 E> 0x1f6acbe241fa @ 0 : a5 StackCheck
40 S> 0x1f6acbe241fb @ 1 : 0c 01 LdaSmi [1]
0x1f6acbe241fd @ 3 : 26 fb Star r0
54 S> 0x1f6acbe241ff @ 5 : 0c 01 LdaSmi [1]
0x1f6acbe24201 @ 7 : 26 fa Star r1
.....
1762 S> 0x1f6acbe243e7 @ 493 : 0c 01 LdaSmi [1]
0x1f6acbe243e9 @ 495 : 26 80 Star r123
1776 S> 0x1f6acbe243eb @ 497 : 0c 01 LdaSmi [1]
0x1f6acbe243ed @ 499 : 00 26 7f ff Star.Wide r124
1790 S> 0x1f6acbe243f1 @ 503 : 0c 01 LdaSmi [1]
.....
14026 S> 0x1f6acbe2586d @ 5747 : 0c 01 LdaSmi [1]
0x1f6acbe2586f @ 5749 : 00 26 14 fc Star.Wide r999
0x1f6acbe25873 @ 5753 : 0d LdaUndefined
14029 S> 0x1f6acbe25874 @ 5754 : a9 Return
到 r124 的時候,就從 Star
變成了 Star.Wide
,為什麼會有這樣子的差異呢?
可以先看到 V8 中的解釋:
// Wide
//
// Prefix bytecode indicating next bytecode has wide (16-bit) operands.
IGNITION_HANDLER(Wide, InterpreterAssembler) {
DispatchWide(OperandScale::kDouble);
}
接著觀察指令的 bytecode:
26 fb Star r0
26 fa Star r1
26 80 Star r123
00 26 7f ff Star.Wide r124
00 26 7e ff Star.Wide r125
很明顯可以看到原本的指令 Star
對應到 26,而後面的參數則是決定要放到第幾個 register,從fb
, fa
...一直到 80
。
而到了 80 之後,可能是用來表示位置的數字不夠用了,因此後面那個參數必須從 8 bit 變成 16 bit,就可以表示更多不同的 register。而前面的 26 也變成 00 26
,需要多一個資訊來表明 Wide
。
這個時候我就滿好奇了,那如果不是 1000 個變數,是 100000 個變數呢?
我們可以用同樣的方式產生十萬個變數,產生的部分 bytecode 會變成這樣:
26 fb Star r0
26 fa Star r1
26 80 Star r123
00 26 7f ff Star.Wide r124
00 26 7e ff Star.Wide r125
00 26 01 80 Star.Wide r32762
00 26 00 80 Star.Wide r32763
01 26 ff 7f ff ff Star.ExtraWide r32764
01 26 fe 7f ff ff Star.ExtraWide r32765
一樣是到了 80
之後數字耗盡,變成 ExtraWide
,指令一樣變成 16 bit,然後後面的參數則是從 16 bit 變成 32 bit。
那如果 32 bit 再耗盡呢?那可要宣告幾億個變數,我猜 heap 會先爆掉。
不同的變數型態
剛剛都只有試了正整數,我們可以來試試幾個不同的型態:
function find_me_test() {
var a = 1 // 正整數
var b = -1 // 負數
var c = 0
var d = 1.1 // 小數
var e = 1e9 // 很大的正整數
var f = -1e9 // 很小的負整數
var g = 'hello'
var h = [5, 6, 7] // 陣列
var i = {yo: 1} // 物件
}
find_me_test()
結果如下:
[generated bytecode for function: find_me_test]
Parameter count 1
Frame size 72
21 E> 0x17de00b1ddea @ 0 : a5 StackCheck
36 S> 0x17de00b1ddeb @ 1 : 0c 01 LdaSmi [1]
0x17de00b1dded @ 3 : 26 fb Star r0
55 S> 0x17de00b1ddef @ 5 : 0c ff LdaSmi [-1]
0x17de00b1ddf1 @ 7 : 26 fa Star r1
74 S> 0x17de00b1ddf3 @ 9 : 0b LdaZero
0x17de00b1ddf4 @ 10 : 26 f9 Star r2
86 S> 0x17de00b1ddf6 @ 12 : 12 00 LdaConstant [0]
0x17de00b1ddf8 @ 14 : 26 f8 Star r3
106 S> 0x17de00b1ddfa @ 16 : 01 0c 00 ca 9a 3b LdaSmi.ExtraWide [1000000000]
0x17de00b1de00 @ 22 : 26 f7 Star r4
130 S> 0x17de00b1de02 @ 24 : 01 0c 00 36 65 c4 LdaSmi.ExtraWide [-1000000000]
0x17de00b1de08 @ 30 : 26 f6 Star r5
155 S> 0x17de00b1de0a @ 32 : 12 01 LdaConstant [1]
0x17de00b1de0c @ 34 : 26 f5 Star r6
173 S> 0x17de00b1de0e @ 36 : 7a 02 00 25 CreateArrayLiteral [2], [0], #37
0x17de00b1de12 @ 40 : 26 f4 Star r7
199 S> 0x17de00b1de14 @ 42 : 7d 03 01 29 CreateObjectLiteral [3], [1], #41
0x17de00b1de18 @ 46 : 26 f3 Star r8
0x17de00b1de1a @ 48 : 0d LdaUndefined
213 S> 0x17de00b1de1b @ 49 : a9 Return
Constant pool (size = 4)
0x17de00b1dd49: [FixedArray] in OldSpace
- map: 0x17de21d007b1 <Map>
- length: 4
0: 0x17de00b1dd79 <HeapNumber 1.1>
1: 0x17de00b1dc91 <String[#5]: hello>
2: 0x17de00b1dd21 <ArrayBoilerplateDescription 0, 0x17de3618b071 <FixedArray[3]>>
3: 0x17de00b1dcf9 <ObjectBoilerplateDescription[3]>
Handler Table (size = 0)
底下提供一個簡單對照版本:
function find_me_test() {
var a = 1 // LdaSmi [1]
var b = -1 // LdaSmi [-1]
var c = 0 // LdaZero
var d = 1.1 // LdaConstant [0]
var e = 1e9 // LdaSmi.ExtraWide [1000000000]
var f = -1e9 // LdaSmi.ExtraWide [-1000000000]
var g = 'hello' // LdaConstant [1]
var h = [5, 6, 7] // CreateArrayLiteral [2], [0], #37
var i = {yo: 1} // CreateObjectLiteral [3], [1], #41
}
/*
Constant pool (size = 4)
0x17de00b1dd49: [FixedArray] in OldSpace
- map: 0x17de21d007b1 <Map>
- length: 4
0: 0x17de00b1dd79 <HeapNumber 1.1>
1: 0x17de00b1dc91 <String[#5]: hello>
2: 0x17de00b1dd21 <ArrayBoilerplateDescription 0, 0x17de3618b071 <FixedArray[3]>>
3: 0x17de00b1dcf9 <ObjectBoilerplateDescription[3]>
*/
如果是浮點數跟字串,會先把東西放在 constant pool,再利用 LdaConstant <index>
拿出來。
陣列跟物件一樣會把東西放在 constant pool,前者用 CreateArrayLiteral
,後者用 CreateObjectLiteral
。
變數的運算
來看一下不同運算會用什麼指令:
function find_me_test() {
var a = 1 // 正整數
a++
a+=1
a--
a-=1
a*=1
a/=1
a%=1
}
find_me_test()
底下我就直接把 bytecode 對應到操作了:
function find_me_test() {
var a = 1
a++ // Inc [0]
a+=1 // AddSmi [1], [1]
a-- // Dec [2]
a-=1 // SubSmi [1], [3]
a*=1 // MulSmi [1], [4]
a/=1 // DivSmi [1], [5]
a%=1 // ModSmi [1], [6]
}
find_me_test()
都是用不同的指令來做不同操作,從指令的名稱大致上就可以推斷出是什麼操作。
全域變數
在 JavaScript 裡面若是不使用任何關鍵字而直接對一個變數賦值,就會變成全域變數,我們來驗證一下這個行為:
function find_me_test() {
var a = 1
b = 2
}
find_me_test()
bytecode:
[generated bytecode for function: find_me_test]
Parameter count 1
Frame size 8
21 E> 0x2a41bc71dcba @ 0 : a5 StackCheck
36 S> 0x2a41bc71dcbb @ 1 : 0c 01 LdaSmi [1]
0x2a41bc71dcbd @ 3 : 26 fb Star r0
40 S> 0x2a41bc71dcbf @ 5 : 0c 02 LdaSmi [2]
42 E> 0x2a41bc71dcc1 @ 7 : 15 00 00 StaGlobal [0], [0]
0x2a41bc71dcc4 @ 10 : 0d LdaUndefined
46 S> 0x2a41bc71dcc5 @ 11 : a9 Return
Constant pool (size = 1)
0x2a41bc71dc49: [FixedArray] in OldSpace
- map: 0x2a41cf9807b1 <Map>
- length: 1
0: 0x2a41bc71d901 <String[#1]: b>
Handler Table (size = 0)
會使用 StaGlobal
這個指令設置全域的值,跟我們想像的一樣。
結語
這一篇裡面我們嘗試來各種變數相關的指令,讓我們對變數的 bytecode 更熟悉了一些,這一篇所觀察到的一些有趣結論如下:
- SyntaxError 在還沒執行程式碼前就知道有錯,所以不會產生 bytecode
- 當數值太大時,會加上
.Wide
(16 bit) 或是.ExtraWide
(32 bit) - 字串與浮點數都會被放到 constant pool 中,用
LdaConstant
來載入 - 陣列與物件類似,但是載入的方法是
CreateArrayLiteral
與CreateObjectLiteral