Day02:從判斷式看 bytecode


前言

在 Day01 裡面,我們嘗試了許多種變數相關的指令,再來我們要試試看各種判斷式,來看 V8 會把這些判斷式翻譯成什麼形式。

簡單的 if

先來一個最簡單的 if 試試看:

function find_me_test() {
  if (15 > 10) {
    console.log(true)
  } else {
    console.log(false)
  }
}

find_me_test()

結果為:

[generated bytecode for function: find_me_test]
Parameter count 1
Frame size 24
   21 E> 0x2c3d05a9dc9a @    0 : a5                StackCheck 
   28 S> 0x2c3d05a9dc9b @    1 : 0c 0f             LdaSmi [15]
         0x2c3d05a9dc9d @    3 : 26 fb             Star r0
         0x2c3d05a9dc9f @    5 : 0c 0a             LdaSmi [10]
   35 E> 0x2c3d05a9dca1 @    7 : 6a fb 00          TestGreaterThan r0, [0]
         0x2c3d05a9dca4 @   10 : 99 17             JumpIfFalse [23] (0x2c3d05a9dcbb @ 33)
   47 S> 0x2c3d05a9dca6 @   12 : 13 00 01          LdaGlobal [0], [1]
         0x2c3d05a9dca9 @   15 : 26 fa             Star r1
   55 E> 0x2c3d05a9dcab @   17 : 28 fa 01 03       LdaNamedProperty r1, [1], [3]
         0x2c3d05a9dcaf @   21 : 26 fb             Star r0
         0x2c3d05a9dcb1 @   23 : 10                LdaTrue 
         0x2c3d05a9dcb2 @   24 : 26 f9             Star r2
   55 E> 0x2c3d05a9dcb4 @   26 : 59 fb fa f9 05    CallProperty1 r0, r1, r2, [5]
         0x2c3d05a9dcb9 @   31 : 8b 15             Jump [21] (0x2c3d05a9dcce @ 52)
   80 S> 0x2c3d05a9dcbb @   33 : 13 00 01          LdaGlobal [0], [1]
         0x2c3d05a9dcbe @   36 : 26 fa             Star r1
   88 E> 0x2c3d05a9dcc0 @   38 : 28 fa 01 03       LdaNamedProperty r1, [1], [3]
         0x2c3d05a9dcc4 @   42 : 26 fb             Star r0
         0x2c3d05a9dcc6 @   44 : 11                LdaFalse 
         0x2c3d05a9dcc7 @   45 : 26 f9             Star r2
   88 E> 0x2c3d05a9dcc9 @   47 : 59 fb fa f9 07    CallProperty1 r0, r1, r2, [7]
         0x2c3d05a9dcce @   52 : 0d                LdaUndefined 
  103 S> 0x2c3d05a9dccf @   53 : a9                Return 
Constant pool (size = 2)
0x2c3d05a9dc19: [FixedArray] in OldSpace
 - map: 0x2c3d196007b1 <Map>
 - length: 2
           0: 0x2c3dd72100e9 <String[#7]: console>
           1: 0x2c3dd720fbe9 <String[#3]: log>
Handler Table (size = 0)
true

一樣為了方便起見,讓我把程式碼簡化一下:

   21 E> 0x2c3d05a9dc9a @    0 : a5                StackCheck 
   28 S> 0x2c3d05a9dc9b @    1 : 0c 0f             LdaSmi [15]
         0x2c3d05a9dc9d @    3 : 26 fb             Star r0
         0x2c3d05a9dc9f @    5 : 0c 0a             LdaSmi [10]
   35 E> 0x2c3d05a9dca1 @    7 : 6a fb 00          TestGreaterThan r0, [0]
         0x2c3d05a9dca4 @   10 : 99 17             JumpIfFalse [23] (0x2c3d05a9dcbb @ 33)
   47 S> 0x2c3d05a9dca6 @   12 : 13 00 01          console.log(true)
         0x2c3d05a9dcb9 @   31 : 8b 15             Jump [21] (0x2c3d05a9dcce @ 52)
   80 S> 0x2c3d05a9dcbb @   33 : 13 00 01          console.log(false)
         0x2c3d05a9dcce @   52 : 0d                LdaUndefined 
  103 S> 0x2c3d05a9dccf @   53 : a9                Return

前面載入值的部分應該很熟悉,就不用多說了。判斷主要是在這一行:TestGreaterThan r0, [0],這一行指令就是:acc = acc > r0,把判斷後的結果存回 acc 當中。

而下一行 JumpIfFalse [23] (0x2c3d05a9dcbb @ 33) 可以直接看原始碼裡面的註釋:

// JumpIfFalse <imm>
//
// Jump by the number of bytes represented by an immediate operand if the
// accumulator contains false. This only works for boolean inputs, and
// will misbehave if passed arbitrary input values.

如果 acc 裡面的值是 false,就往後跳 <imm> 個 bytes,這邊接的參數是 23,所以會跳到 10+23 = 33 的地方,而 V8 已經貼心地在後面標明了跳轉的目的地:(0x2c3d05a9dcbb @ 33)

以我們的 case 來說,結果會是 true,所以不會跳轉,會繼續往下執行console.log(true),然後進行無條件跳轉:Jump [21] (0x2c3d05a9dcce @ 52),跳到函式結束的地方。

如果有寫過組合語言的話,對於這種形式一定不陌生,這就是經典的 if else 結構。

更簡單的 if

接著我們來試一個更簡單的 case:

function find_me_test() {
  if (true) {
    console.log(true)
  } else {
    console.log(false)
  }
}

find_me_test()

結果:

[generated bytecode for function: find_me_test]
Parameter count 1
Frame size 24
   21 E> 0x34c38d21dc92 @    0 : a5                StackCheck 
   44 S> 0x34c38d21dc93 @    1 : 13 00 00          LdaGlobal [0], [0]
         0x34c38d21dc96 @    4 : 26 fa             Star r1
   52 E> 0x34c38d21dc98 @    6 : 28 fa 01 02       LdaNamedProperty r1, [1], [2]
         0x34c38d21dc9c @   10 : 26 fb             Star r0
         0x34c38d21dc9e @   12 : 10                LdaTrue 
         0x34c38d21dc9f @   13 : 26 f9             Star r2
   52 E> 0x34c38d21dca1 @   15 : 59 fb fa f9 04    CallProperty1 r0, r1, r2, [4]
         0x34c38d21dca6 @   20 : 0d                LdaUndefined 
  100 S> 0x34c38d21dca7 @   21 : a9                Return 
Constant pool (size = 2)
0x34c38d21dc19: [FixedArray] in OldSpace
 - map: 0x34c33bf807b1 <Map>
 - length: 2
           0: 0x34c38cf100e9 <String[#7]: console>
           1: 0x34c38cf0fbe9 <String[#3]: log>
Handler Table (size = 0)
true

很明顯地可以看到 V8 Ignition 在這邊做了優化,因為條件是 true,所以其實根本不需要 if else,因此可以從 bytecode 中看到根本沒有 if else,只留下了 if(true) 後面的那一段程式碼。

而我們一開始的範例 if (15 > 10) 其實也會永遠都是 true,只是 Ignition 沒有在這部分做優化而已(但我猜測 TurboFan 有)。

多個 else if

接著來試試看多個 if else 的情況:

function find_me_test() {
  var a = 'hello'
  if (a === 'yo') {
    console.log('yo')
  } else if (a === 'hey') {
    console.log('hey')
  } else if (a === 'hello') {
    console.log('hello')
  } else {
    console.log('default')
  }
}

find_me_test()

我一樣先把 bytecode 稍微處理一下:

[generated bytecode for function: find_me_test]
Parameter count 1
Frame size 32
   21 E> 0x27b29309dd32 @    0 : a5                StackCheck 
   36 S> 0x27b29309dd33 @    1 : 12 00             LdaConstant [0]
         0x27b29309dd35 @    3 : 26 fb             Star r0
   46 S> 0x27b29309dd37 @    5 : 12 01             LdaConstant [1]
   52 E> 0x27b29309dd39 @    7 : 68 fb 00          TestEqualStrict r0, [0]
         0x27b29309dd3c @   10 : 99 18             JumpIfFalse [24] (0x27b29309dd54 @ 34)
   68 S> 0x27b29309dd3e @   12 : 13 02 01          console.log('yo')
         0x27b29309dd52 @   32 : 8b 50             Jump [80] (0x27b29309dda2 @ 112)
   95 S> 0x27b29309dd54 @   34 : 12 04             LdaConstant [4]
  101 E> 0x27b29309dd56 @   36 : 68 fb 07          TestEqualStrict r0, [7]
         0x27b29309dd59 @   39 : 99 18             JumpIfFalse [24] (0x27b29309dd71 @ 63)
  118 S> 0x27b29309dd5b @   41 : 13 02 01          console.log('hey')
         0x27b29309dd6f @   61 : 8b 33             Jump [51] (0x27b29309dda2 @ 112)
  146 S> 0x27b29309dd71 @   63 : 12 00             LdaConstant [0]
  152 E> 0x27b29309dd73 @   65 : 68 fb 0a          TestEqualStrict r0, [10]
         0x27b29309dd76 @   68 : 99 18             JumpIfFalse [24] (0x27b29309dd8e @ 92)
  171 S> 0x27b29309dd78 @   70 : 13 02 01          console.log('hello')
         0x27b29309dd8c @   90 : 8b 16             Jump [22] (0x27b29309dda2 @ 112)
  207 S> 0x27b29309dd8e @   92 : 13 02 01          console.log('default')
         0x27b29309dda2 @  112 : 0d                LdaUndefined 
  234 S> 0x27b29309dda3 @  113 : a9                Return 
Constant pool (size = 6)
0x27b29309dc79: [FixedArray] in OldSpace
 - map: 0x27b28a1007b1 <Map>
 - length: 6
           0: 0x27b29309dbd1 <String[#5]: hello>
           1: 0x27b29309dbe9 <String[#2]: yo>
           2: 0x27b2f3f900e9 <String[#7]: console>
           3: 0x27b2f3f8fbe9 <String[#3]: log>
           4: 0x27b29309dc01 <String[#3]: hey>
           5: 0x27b28a1038e1 <String[#7]: default>
Handler Table (size = 0)
hello

這個結構也相對簡單,比較的形式都是一樣的:

LdaConstant [1]
TestEqualStrict r0, [0]
JumpIfFalse [24] (0x27b29309dd54 @ 34) // 跳到下一個判斷
console.log('yo')
Jump [80] (0x27b29309dda2 @ 112) // 跳到結尾

先跟某個字串做比較,若是成功的話就執行程式碼,然後跳到結尾。比對失敗的話就跳到下一個判斷。

在看這一段 bytecode 的時候,其實會發現 V8 共用了 constant pool 裡面的東西,例如說 if (a === 'yo')console.log('yo') 的那個 yo 都是指向 constant pool 中的同一個物件。

於是我想來做一個小實驗看看:

function find_me_test() {
  var a = 'hello'
  var b = 'hello'
  var c = 'hello'
  if (c === 'hello') {
    console.log('hello')
  }
}

find_me_test()

得到的結果為:

[generated bytecode for function: find_me_test]
Parameter count 1
Frame size 48
   21 E> 0x1823f781dd02 @    0 : a5                StackCheck 
   36 S> 0x1823f781dd03 @    1 : 12 00             LdaConstant [0]
         0x1823f781dd05 @    3 : 26 fb             Star r0
   54 S> 0x1823f781dd07 @    5 : 12 00             LdaConstant [0]
         0x1823f781dd09 @    7 : 26 fa             Star r1
   72 S> 0x1823f781dd0b @    9 : 12 00             LdaConstant [0]
         0x1823f781dd0d @   11 : 26 f9             Star r2
   82 S> 0x1823f781dd0f @   13 : 12 00             LdaConstant [0]
   88 E> 0x1823f781dd11 @   15 : 68 f9 00          TestEqualStrict r2, [0]
         0x1823f781dd14 @   18 : 99 16             JumpIfFalse [22] (0x1823f781dd2a @ 40)
  107 S> 0x1823f781dd16 @   20 : 13 01 01          LdaGlobal [1], [1]
         0x1823f781dd19 @   23 : 26 f7             Star r4
  115 E> 0x1823f781dd1b @   25 : 28 f7 02 03       LdaNamedProperty r4, [2], [3]
         0x1823f781dd1f @   29 : 26 f8             Star r3
         0x1823f781dd21 @   31 : 12 00             LdaConstant [0]
         0x1823f781dd23 @   33 : 26 f6             Star r5
  115 E> 0x1823f781dd25 @   35 : 59 f8 f7 f6 05    CallProperty1 r3, r4, r5, [5]
         0x1823f781dd2a @   40 : 0d                LdaUndefined 
  132 S> 0x1823f781dd2b @   41 : a9                Return 
Constant pool (size = 3)
0x1823f781dc79: [FixedArray] in OldSpace
 - map: 0x18239b2007b1 <Map>
 - length: 3
           0: 0x1823f781dc01 <String[#5]: hello>
           1: 0x1823c6b900e9 <String[#7]: console>
           2: 0x1823c6b8fbe9 <String[#3]: log>
Handler Table (size = 0)
hello

事實證明,裡面所有出現的 hello 都是指向同一個地方,因此儘管程式碼裡面有幾千個 hello,constant pool 裡面永遠都只會有一個,無論你用了一個 hello 還是一百個 hello,它們佔用的空間是一樣的。

switch case

接著來看一下 switch case,為了方便對照,這邊的邏輯跟上面的 if else 的範例是一模一樣的:

function find_me_test() {
  var a = 'hello'
  switch(a) {
    case 'yo':
      console.log('yo')
      break;
    case 'hey': 
      console.log('hey')
      break
    case 'hello':
      console.log('hello')
      break
    default:
      console.log('default')
  }
}

find_me_test()

一樣把 bytecode 先做處理,比較好看:

[generated bytecode for function: find_me_test]
Parameter count 1
Frame size 40
   21 E> 0x37a30dc9dd2a @    0 : a5                StackCheck 
   36 S> 0x37a30dc9dd2b @    1 : 12 00             LdaConstant [0]
         0x37a30dc9dd2d @    3 : 26 fb             Star r0
   46 S> 0x37a30dc9dd2f @    5 : 12 01             LdaConstant [1]
         0x37a30dc9dd31 @    7 : 68 fb 00          TestEqualStrict r0, [0]
         0x37a30dc9dd34 @   10 : 27 fb fa          Mov r0, r1
         0x37a30dc9dd37 @   13 : 98 12             JumpIfTrue [18] (0x37a30dc9dd49 @ 31)
         0x37a30dc9dd39 @   15 : 12 02             LdaConstant [2]
         0x37a30dc9dd3b @   17 : 68 fa 00          TestEqualStrict r1, [0]
         0x37a30dc9dd3e @   20 : 98 21             JumpIfTrue [33] (0x37a30dc9dd5f @ 53)
         0x37a30dc9dd40 @   22 : 12 00             LdaConstant [0]
         0x37a30dc9dd42 @   24 : 68 fa 00          TestEqualStrict r1, [0]
         0x37a30dc9dd45 @   27 : 98 30             JumpIfTrue [48] (0x37a30dc9dd75 @ 75)
         0x37a30dc9dd47 @   29 : 8b 44             Jump [68] (0x37a30dc9dd8b @ 97)
   79 S> 0x37a30dc9dd49 @   31 : 13 03 01          console.log('yo')
  103 S> 0x37a30dc9dd5d @   51 : 8b 42             Jump [66] (0x37a30dc9dd9f @ 117)
  133 S> 0x37a30dc9dd5f @   53 : 13 03 01          console.log('hey')
  158 S> 0x37a30dc9dd73 @   73 : 8b 2c             Jump [44] (0x37a30dc9dd9f @ 117)
  188 S> 0x37a30dc9dd75 @   75 : 13 03 01          console.log('hello')
  215 S> 0x37a30dc9dd89 @   95 : 8b 16             Jump [22] (0x37a30dc9dd9f @ 117)
  240 S> 0x37a30dc9dd8b @   97 : 13 03 01          console.log('default')
         0x37a30dc9dd9f @  117 : 0d                LdaUndefined 
  267 S> 0x37a30dc9dda0 @  118 : a9                Return 
Constant pool (size = 6)
0x37a30dc9dc79: [FixedArray] in OldSpace
 - map: 0x37a3e6e807b1 <Map>
 - length: 6
           0: 0x37a30dc9dbd1 <String[#5]: hello>
           1: 0x37a30dc9dbe9 <String[#2]: yo>
           2: 0x37a30dc9dc01 <String[#3]: hey>
           3: 0x37a3de5900e9 <String[#7]: console>
           4: 0x37a3de58fbe9 <String[#3]: log>
           5: 0x37a3e6e838e1 <String[#7]: default>
Handler Table (size = 0)
hello

可以看到 switch case 的結構與 if else 不太一樣,switch case 的結構是:

if (a === 'yo') goto yo
if (a === 'hey') goto hey
if (a === 'hello') goto hello
goto default

yo:
console.log('yo')
goto return

hey:
console.log('hey')
goto return

hello:
console.log('hello')
goto return

default:
console.log('default')

return:
return undefined

先把判斷式全部都放在一起,如果符合的話就跳到執行的區塊去,執行完以後結束;不符合的話就看下一個判斷式。所以判斷式全部在一起,然後需要執行的程式碼也全部都放一起。

而 if else 的結構是這樣的:

if (a !== 'yo') goto next if
console.log('yo')
goto return

if (a !== 'hey') goto next if
console.log('hey')
goto return

if (a !== 'hello') goto default
console.log('hello')
goto return

default:
console.log('default')

return:
return undefined

它把要執行的程式碼跟判斷式放在一塊,如果不符合條件就跳到下一個 if 去。所以很奇妙地,switch case 與 if else 的 bytecode 邏輯是相反的,一個是:「如果...為 true」,一個是:「如果...為 false」。

那為什麼會有這種差異呢?我懷疑跟 switch case 的特性:break 有關,因此把其中一個 break 拿掉來測試,順便把條件換了一下:

function find_me_test() {
  var a = 'yo'
  switch(a) {
    case 'yo':
      console.log('yo')
    case 'hey': 
      console.log('hey')
      break
    case 'hello':
      console.log('hello')
      break
    default:
      console.log('default')
  }
}

find_me_test()

先來講講如果忘記放 break 會發生什麼事情,如果忘記放的話,程式碼就會繼續執行,而且「不管下一個 case 的條件符合與否,都會執行,直到碰到 break 為止」

因此上面的程式碼跑進去 case 'yo' 那一段之後,會繼續往下跑到 case 'hello' 那一段,印出 hello 之後才結束。

產生出來的 bytecode 如下:

[generated bytecode for function: find_me_test]
Parameter count 1
Frame size 40
   21 E> 0x38c820f9dd2a @    0 : a5                StackCheck 
   36 S> 0x38c820f9dd2b @    1 : 12 00             LdaConstant [0]
         0x38c820f9dd2d @    3 : 26 fb             Star r0
   43 S> 0x38c820f9dd2f @    5 : 12 00             LdaConstant [0]
         0x38c820f9dd31 @    7 : 68 fb 00          TestEqualStrict r0, [0]
         0x38c820f9dd34 @   10 : 27 fb fa          Mov r0, r1
         0x38c820f9dd37 @   13 : 98 12             JumpIfTrue [18] (0x38c820f9dd49 @ 31)
         0x38c820f9dd39 @   15 : 12 01             LdaConstant [1]
         0x38c820f9dd3b @   17 : 68 fa 00          TestEqualStrict r1, [0]
         0x38c820f9dd3e @   20 : 98 1f             JumpIfTrue [31] (0x38c820f9dd5d @ 51)
         0x38c820f9dd40 @   22 : 12 02             LdaConstant [2]
         0x38c820f9dd42 @   24 : 68 fa 00          TestEqualStrict r1, [0]
         0x38c820f9dd45 @   27 : 98 2e             JumpIfTrue [46] (0x38c820f9dd73 @ 73)
         0x38c820f9dd47 @   29 : 8b 42             Jump [66] (0x38c820f9dd89 @ 95)
   76 S> 0x38c820f9dd49 @   31 : 13 03 01          console.log('yo')
  117 S> 0x38c820f9dd5d @   51 : 13 03 01          console.log('hey')
  142 S> 0x38c820f9dd71 @   71 : 8b 2c             Jump [44] (0x38c820f9dd9d @ 115)
  172 S> 0x38c820f9dd73 @   73 : 13 03 01          console.log('hello')
  199 S> 0x38c820f9dd87 @   93 : 8b 16             Jump [22] (0x38c820f9dd9d @ 115)
  224 S> 0x38c820f9dd89 @   95 : 13 03 01          consle.log('default')
         0x38c820f9dd9d @  115 : 0d                LdaUndefined 
  251 S> 0x38c820f9dd9e @  116 : a9                Return 
Constant pool (size = 6)
0x38c820f9dc79: [FixedArray] in OldSpace
 - map: 0x38c840f007b1 <Map>
 - length: 6
           0: 0x38c820f9dbd1 <String[#2]: yo>
           1: 0x38c820f9dbe9 <String[#3]: hey>
           2: 0x38c820f9dc01 <String[#5]: hello>
           3: 0x38c8c27900e9 <String[#7]: console>
           4: 0x38c8c278fbe9 <String[#3]: log>
           5: 0x38c840f038e1 <String[#7]: default>
Handler Table (size = 0)
yo
hey

跟一般正常的 switch case 比起來,差在哪邊?差在 console.log('yo')console.log('hey') 之間沒有 jump,所以兩個段落都會被執行到。所以 break 在這邊的作用就是在每一個段落間都加上一個 jump,避免重複執行。

或是換句話說,就是上面我們提到的同一個結構,只是把 yo 那一段的 goto return 拿掉。

if (a === 'yo') goto yo
if (a === 'hey') goto hey
if (a === 'hello') goto hello
goto default

yo:
console.log('yo')
goto return // 這行拿掉

hey:
console.log('hey')
goto return

hello:
console.log('hello')
goto return

default:
console.log('default')

return:
return undefined

看到這個特性,就不難理解為什麼 if else 與 switch case 的結構長得不一樣了。因為如果是採用跟 if else 相同的結構,很難達成這一個功能,沒辦法在進入某一個條件之後,無條件去執行下一個段落的程式碼。

除此之外,switch case 還有一個奇妙的地方是:Mov r0, r1,會把 r0 的值搬到 r1 去,但其實光從 bytecode,我看不出來為什麼要這樣做,因為把值留在 r0 也是完全 ok 的。

三元運算子

最後,我們來試試看三元運算子:

function find_me_test() {
  var a = 2 > 1 ? 'first' : 'second'
}

find_me_test()

結果:

[generated bytecode for function: find_me_test]
Parameter count 1
Frame size 16
   21 E> 0x3db96639dca2 @    0 : a5                StackCheck 
   36 S> 0x3db96639dca3 @    1 : 0c 02             LdaSmi [2]
         0x3db96639dca5 @    3 : 26 fa             Star r1
         0x3db96639dca7 @    5 : 0c 01             LdaSmi [1]
   38 E> 0x3db96639dca9 @    7 : 6a fa 00          TestGreaterThan r1, [0]
         0x3db96639dcac @   10 : 99 06             JumpIfFalse [6] (0x3db96639dcb2 @ 16)
         0x3db96639dcae @   12 : 12 00             LdaConstant [0]
         0x3db96639dcb0 @   14 : 8b 04             Jump [4] (0x3db96639dcb4 @ 18)
         0x3db96639dcb2 @   16 : 12 01             LdaConstant [1]
         0x3db96639dcb4 @   18 : 26 fb             Star r0
         0x3db96639dcb6 @   20 : 0d                LdaUndefined 
   63 S> 0x3db96639dcb7 @   21 : a9                Return 
Constant pool (size = 2)
0x3db96639dc31: [FixedArray] in OldSpace
 - map: 0x3db95d5007b1 <Map>
 - length: 2
           0: 0x3db9e389da79 <String[#5]: first>
           1: 0x3db95d503239 <String[#6]: second>
Handler Table (size = 0)

乍看之下其實跟 if else 的結構很類似,但其實有一個很大的差別。我們先來看一下 if else 版本會長怎樣:

function find_me_test() {
  if (2 > 1) {
    var a = 'first'
  } else {
    var a = 'second'
  }
}

find_me_test()
[generated bytecode for function: find_me_test]
Parameter count 1
Frame size 16
   21 E> 0x37542301dcaa @    0 : a5                StackCheck 
   28 S> 0x37542301dcab @    1 : 0c 02             LdaSmi [2]
         0x37542301dcad @    3 : 26 fa             Star r1
         0x37542301dcaf @    5 : 0c 01             LdaSmi [1]
   34 E> 0x37542301dcb1 @    7 : 6a fa 00          TestGreaterThan r1, [0]
         0x37542301dcb4 @   10 : 99 08             JumpIfFalse [8] (0x37542301dcbc @ 18)
   53 S> 0x37542301dcb6 @   12 : 12 00             LdaConstant [0]
         0x37542301dcb8 @   14 : 26 fb             Star r0
         0x37542301dcba @   16 : 8b 06             Jump [6] (0x37542301dcc0 @ 22)
   84 S> 0x37542301dcbc @   18 : 12 01             LdaConstant [1]
         0x37542301dcbe @   20 : 26 fb             Star r0
         0x37542301dcc0 @   22 : 0d                LdaUndefined 
   97 S> 0x37542301dcc1 @   23 : a9                Return 
Constant pool (size = 2)
0x37542301dc31: [FixedArray] in OldSpace
 - map: 0x3754e7e807b1 <Map>
 - length: 2
           0: 0x37549ac9da79 <String[#5]: first>
           1: 0x3754e7e83239 <String[#6]: second>
Handler Table (size = 0)

大家有看出差別嗎?if else 的本質還是兩個不同的程式碼區塊,所以兩塊都會有兩個動作:LdaConstantStar r0

而三元運算子已經預設是要賦值了,所以在 bytecode 裡面的分支只決定了要載入哪一個值到 acc 裡面去,決定以後再統一 Star r0,因此從頭到尾只會有一個 Star r0

那三元運算子是否也會像 if 那樣做優化呢?

function find_me_test() {
  var a = true ? 'first' : 'second'
}

find_me_test()

結果:

[generated bytecode for function: find_me_test]
Parameter count 1
Frame size 8
   21 E> 0x2118d919dc9a @    0 : a5                StackCheck 
   36 S> 0x2118d919dc9b @    1 : 12 00             LdaConstant [0]
         0x2118d919dc9d @    3 : 26 fb             Star r0
         0x2118d919dc9f @    5 : 0d                LdaUndefined 
   62 S> 0x2118d919dca0 @    6 : a9                Return 
Constant pool (size = 1)
0x2118d919dc31: [FixedArray] in OldSpace
 - map: 0x211828d007b1 <Map>
 - length: 1
           0: 0x21186809da79 <String[#5]: first>
Handler Table (size = 0)

答案是會的。對於已經知道的結果,便不會再產生分支,而是直接賦值。

結語

在這一篇文章中我們觀察了各種判斷式的特性,得到了一些有趣的結論:

  1. 對於 if (true) 這種 case,V8 Ignition 會做優化,if (2 > 1) 則不會
  2. if else 的結構與 switch case 不一樣,後者的結構猜測是為了 break 而定
  3. 三元運算子的本質跟 if else 差不多
  4. 在程式碼中同一個字串是可以被重複利用的,都是指向同一個 constant pool 的元素
#javascript
V8 在處理 JavaScript 的程式碼時,會先將程式碼轉換成中間碼(bytecode)才執行。因此,藉由觀察 V8 bytecode,可以更理解一段 JavaScript 程式碼在 V8 眼裡是什麼樣子。這個系列文會以一系列的簡單程式碼為例,帶大家一起研究 bytecode






Related Posts

用 JavaScript 學習資料結構和演算法:陣列(Array)篇

用 JavaScript 學習資料結構和演算法:陣列(Array)篇

來寫測試吧!

來寫測試吧!

MTR04 W2 D17 陣列練習題

MTR04 W2 D17 陣列練習題



Comments