前回
JavaScriptで、なぜfor文の初期化部分においてletで宣言された変数はループごとに異なるインスタンスを持ちうるのかの続き。
疑問
前回の調査で、規格上では「ループ各回で環境が作成される」ことがわかった。そうすると「結構コスト高な気が…」という気がしてくる。
実際、前回の例のようにクロージャがループ内で作成される場合は、ループ各回で作成されるクロージャ内でそれぞれの環境を参照しているため、ループ各回で環境の生成が必要となる。
const functions = [];
for (let i = 0; i < 3; i++) {
functions.push(() => console.log(i));
}
functions[0]();
functions[1]();
functions[2]();
だが、以下のようなfor
for (let i = 0; i < 3; i++) {
console.log(i);
}
で、クロージャ作成が無い場合は、ループ各回で作成した環境は他のどこからも参照されないので、ループ内の環境は一つでも動作は同じである。最適化してくれるのではないか?
調査
実際にNode.jsでどう処理されるか調べてみた。Node.jsはV8を実行エンジンとして採用している。最適化するかどうかに関してはV8での話限定になってしまうが、最適化されるか? という調査観点からすれば十分である。
V8
基本動作はFiring up the Ignition interpreterで述べられている。
通常JavaScriptエンジンはJITコンパイルしてスクリプトを実行するが、JITコンパイルされたコードはメモリをたくさん使ってしまう場合があるなどオーバーヘッドがある。多くはこのオーバーヘッドは無駄なので、V8ではスクリプトをバイトコード列にコンパイルし、そのバイトコードを実行するインタプリタ(Ignition)を作った。
(実行回数が多いなどパフォーマンス上影響が大きい部分は最適化したJITコンパイラ(Crankshaft/TurboFan)でコンパイルしたコードを実行する)
結局はIgnitionのバイトコードがスクリプト実行の元となるので、ここで生成されたバイトコードを見れば動きがわかるはずだ。
Ignition
Ignitionはレジスタマシンで、特別なレジスタであるaccumulatorを持った構造である。JVMなどはスタックマシンであるが、レジスタマシン/スタックマシンの違いによりバイトコードのサイズや実行速度などが変わってくる。レジスタマシンは、オペランドの指定が必要なためバイトコードサイズが大きくなるが、実行速度が速い傾向がある。Ignitionでは自動的に入出力指定されるaccumulatorを持たせることでオペランド指定を減らし、実行速度を維持しつつバイトコードサイズも減らそうとしたのだろう。
Ignition Design Doc
In order to reduce the size of the bytecode stream, Ignition has an accumulator register, which is used by many bytecodes as implicit input and output register.
ニーモニック
バイトコードに関する概要がUnderstanding V8's Bytecodeで述べられている。
よく出てくる略語は以下。
略語 | 実際の語 | 意味 |
---|---|---|
Ld | Load | メモリや直値でレジスタに値を読み込む |
St | Store | レジスタからメモリやレジスタに値を書き込む |
a | accumulator | accumulatorを対象レジスタとして指定する |
Smi | Small integer | 整数直値 |
r | register | レジスタ指定 |
これらの略語を勘案すると、なんとなく以下のような気がしてくる。
ニーモニック | 意味 |
---|---|
LdaZero | accumulatorの値を0にする |
LdaSmi 3 | accumulatorの値を3にする |
Star r2 | accumulatorの値をr2に書き込む |
LdaGlobal 0 | Global環境で0で指定されるオブジェクトをaccumulatorに読み込む |
LdaNamedProperty r2 1 | r2が指すオブジェクトにおいて1で指定されるプロパティをaccumulatorに読み込む |
ぱっと見でわかりそうなものもある。
ニーモニック | 意味 |
---|---|
TestLessThan r0 | accumulatorの値がr0の値より小かどうか判定(し、結果をフラグなどに記録) |
JumpIfFalse 28 | フラグを見てfalseだったら28ワード先にジャンプする |
Inc | accumulatorの値に1を加算する |
など。正確な動作はV8のソースとか見ればわかるだろうが、今回の目的はそこまでは必要ないので略する。
バイトコードを見る
読める気になったところで、実際にバイトコードを見てみる。
クロージャを作る場合
まずはループ各回で環境が作られる様子を確認したい。以下のjsファイルを作る。
const letFunctions = [];
function letClosures() {
for (let i = 0; i < 3; i++) {
letFunctions.push(() => console.log(i));
}
}
letClosures();
letFunctions.forEach(f => f());
Node.jsでコンパイルする。--print-bytecode
オプションでバイトコードが出力される。これだけだと出力が膨大になるので、--print-bytecode-filter
オプションを指定して出力範囲を見たい関数だけにする。
% node --print-bytecode --print-bytecode-filter=letClosures let-for-closures.js
[generated bytecode for function: letClosures (0x0a271c6a0e31 <SharedFunctionInfo letClosures>)]
Parameter count 1
Register count 8
Frame size 64
45 E> 0xa271c6a1656 @ 0 : a7 StackCheck
65 S> 0xa271c6a1657 @ 1 : 0b LdaZero
0xa271c6a1658 @ 2 : 26 f8 Star r3
0xa271c6a165a @ 4 : 26 fb Star r0
0xa271c6a165c @ 6 : 0c 01 LdaSmi [1]
0xa271c6a165e @ 8 : 26 fa Star r1
130 E> 0xa271c6a1660 @ 10 : a7 StackCheck
0xa271c6a1661 @ 11 : 82 00 CreateBlockContext [0]
0xa271c6a1663 @ 13 : 16 f7 PushContext r4
0xa271c6a1665 @ 15 : 0f LdaTheHole
0xa271c6a1666 @ 16 : 1d 04 StaCurrentContextSlot [4]
0xa271c6a1668 @ 18 : 25 fb Ldar r0
0xa271c6a166a @ 20 : 1d 04 StaCurrentContextSlot [4]
0xa271c6a166c @ 22 : 0c 01 LdaSmi [1]
0xa271c6a166e @ 24 : 67 fa 00 TestEqual r1, [0]
0xa271c6a1671 @ 27 : 9a 07 JumpIfFalse [7] (0xa271c6a1678 @ 34)
0xa271c6a1673 @ 29 : 0b LdaZero
0xa271c6a1674 @ 30 : 26 fa Star r1
0xa271c6a1676 @ 32 : 8b 08 Jump [8] (0xa271c6a167e @ 40)
76 S> 0xa271c6a1678 @ 34 : 1a 04 LdaCurrentContextSlot [4]
0xa271c6a167a @ 36 : 4c 01 Inc [1]
76 E> 0xa271c6a167c @ 38 : 1d 04 StaCurrentContextSlot [4]
0xa271c6a167e @ 40 : 0c 01 LdaSmi [1]
0xa271c6a1680 @ 42 : 26 f9 Star r2
70 S> 0xa271c6a1682 @ 44 : 1a 04 LdaCurrentContextSlot [4]
0xa271c6a1684 @ 46 : 26 f6 Star r5
0xa271c6a1686 @ 48 : 0c 03 LdaSmi [3]
70 E> 0xa271c6a1688 @ 50 : 69 f6 02 TestLessThan r5, [2]
0xa271c6a168b @ 53 : 9a 04 JumpIfFalse [4] (0xa271c6a168f @ 57)
0xa271c6a168d @ 55 : 8b 06 Jump [6] (0xa271c6a1693 @ 61)
0xa271c6a168f @ 57 : 17 f7 PopContext r4
0xa271c6a1691 @ 59 : 8b 3d Jump [61] (0xa271c6a16ce @ 120)
0xa271c6a1693 @ 61 : 0c 01 LdaSmi [1]
0xa271c6a1695 @ 63 : 67 f9 03 TestEqual r2, [3]
0xa271c6a1698 @ 66 : 9a 26 JumpIfFalse [38] (0xa271c6a16be @ 104)
52 E> 0xa271c6a169a @ 68 : a7 StackCheck
87 S> 0xa271c6a169b @ 69 : 19 f7 04 00 LdaImmutableContextSlot r4, [4], [0]
0xa271c6a169f @ 73 : ac 01 ThrowReferenceErrorIfHole [1]
0xa271c6a16a1 @ 75 : 26 f5 Star r6
100 E> 0xa271c6a16a3 @ 77 : 28 f5 02 04 LdaNamedProperty r6, [2], [4]
0xa271c6a16a7 @ 81 : 26 f6 Star r5
0xa271c6a16a9 @ 83 : 81 03 00 02 CreateClosure [3], [0], #2
0xa271c6a16ad @ 87 : 26 f4 Star r7
100 E> 0xa271c6a16af @ 89 : 59 f6 f5 f4 06 CallProperty1 r5, r6, r7, [6]
0xa271c6a16b4 @ 94 : 0b LdaZero
0xa271c6a16b5 @ 95 : 26 f9 Star r2
0xa271c6a16b7 @ 97 : 1a 04 LdaCurrentContextSlot [4]
0xa271c6a16b9 @ 99 : 26 fb Star r0
0xa271c6a16bb @ 101 : 8a 28 01 JumpLoop [40], [1] (0xa271c6a1693 @ 61)
0xa271c6a16be @ 104 : 0c 01 LdaSmi [1]
130 E> 0xa271c6a16c0 @ 106 : 67 f9 08 TestEqual r2, [8]
0xa271c6a16c3 @ 109 : 9a 06 JumpIfFalse [6] (0xa271c6a16c9 @ 115)
0xa271c6a16c5 @ 111 : 17 f7 PopContext r4
0xa271c6a16c7 @ 113 : 8b 07 Jump [7] (0xa271c6a16ce @ 120)
0xa271c6a16c9 @ 115 : 17 f7 PopContext r4
0xa271c6a16cb @ 117 : 8a 6b 00 JumpLoop [107], [0] (0xa271c6a1660 @ 10)
0xa271c6a16ce @ 120 : 0d LdaUndefined
132 S> 0xa271c6a16cf @ 121 : ab Return
Constant pool (size = 4)
Handler Table (size = 0)
0
1
2
ひとつひとつ見ていく。
65 S> 0xa271c6a1657 @ 1 : 0b LdaZero
0xa271c6a1658 @ 2 : 26 f8 Star r3
0xa271c6a165a @ 4 : 26 fb Star r0
0xa271c6a165c @ 6 : 0c 01 LdaSmi [1]
0xa271c6a165e @ 8 : 26 fa Star r1
r0
とr3
を0にし、r1
を1
にしている。この0
はあとの変数i
の初期値に使用する。
130 E> 0x59e8e561660 @ 10 : a7 StackCheck
0x59e8e561661 @ 11 : 82 00 CreateBlockContext [0]
0x59e8e561663 @ 13 : 16 f7 PushContext r4
0x59e8e561665 @ 15 : 0f LdaTheHole
0x59e8e561666 @ 16 : 1d 04 StaCurrentContextSlot [4]
0x59e8e561668 @ 18 : 25 fb Ldar r0
0x59e8e56166a @ 20 : 1d 04 StaCurrentContextSlot [4]
-
0xa271c6a1661
でCreateBlockContext
があり、その後PushContext r4
がある。これは環境を作って一つ中の環境に移っているということ。 - その後、
LdaTheHole
以下で、環境内の変数(i
)を無効値で初期化→r0
の値(=0
)で書き換えている。- (スクリプトの
for (let i = 0;…
の値を1
に書き換えると、r0
が1
になるので、1
がi
の初期値として書き込まれる)
- (スクリプトの
0x59e8e56166c @ 22 : 0c 01 LdaSmi [1]
0x59e8e56166e @ 24 : 67 fa 00 TestEqual r1, [0]
0x59e8e561671 @ 27 : 9a 07 JumpIfFalse [7] (0x59e8e561678 @ 34)
0x59e8e561673 @ 29 : 0b LdaZero
0x59e8e561674 @ 30 : 26 fa Star r1
0x59e8e561676 @ 32 : 8b 08 Jump [8] (0x59e8e56167e @ 40)
76 S> 0x59e8e561678 @ 34 : 1a 04 LdaCurrentContextSlot [4]
0x59e8e56167a @ 36 : 4c 01 Inc [1]
76 E> 0x59e8e56167c @ 38 : 1d 04 StaCurrentContextSlot [4]
- 最初は
r1
の値は1
なので、JumpIfFalse
ではジャンプしない。r1
を0
にして0x59e8e56167e
へジャンプ。このジャンプは、+1
(Inc
)する処理を初回は飛ばすように機能する。これにより、すでに初期値が入っているところに余計に+1
されないようにしている。 - 2回目以降に来たときは
r1
が0
になっているので、変数i
を+1
する処理が入る。
0x59e8e56167e @ 40 : 0c 01 LdaSmi [1]
0x59e8e561680 @ 42 : 26 f9 Star r2
70 S> 0x59e8e561682 @ 44 : 1a 04 LdaCurrentContextSlot [4]
0x59e8e561684 @ 46 : 26 f6 Star r5
0x59e8e561686 @ 48 : 0c 03 LdaSmi [3]
70 E> 0x59e8e561688 @ 50 : 69 f6 02 TestLessThan r5, [2]
0x59e8e56168b @ 53 : 9a 04 JumpIfFalse [4] (0x59e8e56168f @ 57)
0x59e8e56168d @ 55 : 8b 06 Jump [6] (0x59e8e561693 @ 61)
0x59e8e56168f @ 57 : 17 f7 PopContext r4
0x59e8e561691 @ 59 : 8b 3d Jump [61] (0x59e8e5616ce @ 120)
-
r2
を1
にし、 - 変数
i
の値をr5
に入れる。 -
r5
を3
と比較して、(3 < r5
が)falseなら0x2ec0ffaa168f
にジャンプ。ジャンプするとPopContext
して環境を一つ外に移して最後の方にジャンプしている。ということはループを抜ける処理である。つまり「変数i
が3
より小でないならループを終わる 」なので、forのループ判定をここでやっていることがわかる。
0x59e8e561693 @ 61 : 0c 01 LdaSmi [1]
0x59e8e561695 @ 63 : 67 f9 03 TestEqual r2, [3]
0x59e8e561698 @ 66 : 9a 26 JumpIfFalse [38] (0x59e8e5616be @ 104)
52 E> 0x59e8e56169a @ 68 : a7 StackCheck
87 S> 0x59e8e56169b @ 69 : 19 f7 04 00 LdaImmutableContextSlot r4, [4], [0]
0x59e8e56169f @ 73 : ac 01 ThrowReferenceErrorIfHole [1]
0x59e8e5616a1 @ 75 : 26 f5 Star r6
100 E> 0x59e8e5616a3 @ 77 : 28 f5 02 04 LdaNamedProperty r6, [2], [4]
0x59e8e5616a7 @ 81 : 26 f6 Star r5
0x59e8e5616a9 @ 83 : 81 03 00 02 CreateClosure [3], [0], #2
0x59e8e5616ad @ 87 : 26 f4 Star r7
100 E> 0x59e8e5616af @ 89 : 59 f6 f5 f4 06 CallProperty1 r5, r6, r7, [6]
-
r2
が1
でなかったら0x59e8e5616be
にジャンプするが、いまは1
なので進む。 - ImmutableContextSlotから
r6
に値を格納。Immutableなので、変数letFunctions
である。letFunctions
の中のプロパティをr5
に入れて、クロージャを作ってr5
を呼び出している。r5
は関数ということなので、これはletFunctions.push(() => console.log(i))
におけるpush()
の呼び出しである。作ったクロージャを配列に追加しているところ。
0x59e8e5616b4 @ 94 : 0b LdaZero
0x59e8e5616b5 @ 95 : 26 f9 Star r2
0x59e8e5616b7 @ 97 : 1a 04 LdaCurrentContextSlot [4]
0x59e8e5616b9 @ 99 : 26 fb Star r0
0x59e8e5616bb @ 101 : 8a 28 01 JumpLoop [40], [1] (0x59e8e561693 @ 61)
-
r2
を0
にして、変数i
の値をr0
へ。その後0x59e8e561693
へ戻る。 -
0x59e8e561693
ではr2
が0
なので、今回は0x59e8e5616be
へ。
0x59e8e5616be @ 104 : 0c 01 LdaSmi [1]
130 E> 0x59e8e5616c0 @ 106 : 67 f9 08 TestEqual r2, [8]
0x59e8e5616c3 @ 109 : 9a 06 JumpIfFalse [6] (0x59e8e5616c9 @ 115)
0x59e8e5616c5 @ 111 : 17 f7 PopContext r4
0x59e8e5616c7 @ 113 : 8b 07 Jump [7] (0x59e8e5616ce @ 120)
0x59e8e5616c9 @ 115 : 17 f7 PopContext r4
0x59e8e5616cb @ 117 : 8a 6b 00 JumpLoop [107], [0] (0x59e8e561660 @ 10)
-
r2
が0
なのでまたジャンプ。0x59e8e5616c9
へ。 -
0x59e8e5616c9
ではPopContext
して環境を一つ外にし、その後ループの先頭の方、0x59e8e561660
にジャンプ。 -
0x59e8e561660
では、新しく環境を作って、変数i
の値をr0
で初期化。r0
はループ途中の値でいまは1
なので、i
の値が1
として新しい環境が作られる。ループが進むたびに+1
されていくので、前回のループ時の環境の図のようになっている。
0x59e8e5616ce @ 120 : 0d LdaUndefined
132 S> 0x59e8e5616cf @ 121 : ab Return
- ループが終わるのは
i >= 3
のとき。0x59e8e56168f
ではPopContext
して環境を一つ外にして0x59e8e5616ce
に来る。 -
LdaUndefined
では関数letClosures()
の戻り値を設定している。この関数は値を返さないので、undefined
となる。関数の戻り値はaccumulatorで受け渡しされるようだ。
ループ内でクロージャを作る場合は、ループ各回で環境が作成されていそうなことがわかった。つまり、変数i
はループ各回で異なるインスタンスになっている。
クロージャを作らない場合
つぎはループ内でクロージャを作らない場合。以下のjsファイルを作る。
function letFor() {
for (let i = 0; i < 3; i++) {
console.log(i);
}
}
letFor();
同様にバイトコードを出力する。
% node --print-bytecode --print-bytecode-filter=letFor let-for-simple.js
[generated bytecode for function: letFor (0x0f1943760df1 <SharedFunctionInfo letFor>)]
Parameter count 1
Register count 3
Frame size 24
15 E> 0xf19437614be @ 0 : a7 StackCheck
35 S> 0xf19437614bf @ 1 : 0b LdaZero
0xf19437614c0 @ 2 : 26 fb Star r0
40 S> 0xf19437614c2 @ 4 : 0c 03 LdaSmi [3]
40 E> 0xf19437614c4 @ 6 : 69 fb 00 TestLessThan r0, [0]
0xf19437614c7 @ 9 : 9a 1c JumpIfFalse [28] (0xf19437614e3 @ 37)
22 E> 0xf19437614c9 @ 11 : a7 StackCheck
57 S> 0xf19437614ca @ 12 : 13 00 01 LdaGlobal [0], [1]
0xf19437614cd @ 15 : 26 f9 Star r2
65 E> 0xf19437614cf @ 17 : 28 f9 01 03 LdaNamedProperty r2, [1], [3]
0xf19437614d3 @ 21 : 26 fa Star r1
65 E> 0xf19437614d5 @ 23 : 59 fa f9 fb 05 CallProperty1 r1, r2, r0, [5]
46 S> 0xf19437614da @ 28 : 25 fb Ldar r0
0xf19437614dc @ 30 : 4c 07 Inc [7]
0xf19437614de @ 32 : 26 fb Star r0
0xf19437614e0 @ 34 : 8a 1e 00 JumpLoop [30], [0] (0xf19437614c2 @ 4)
0xf19437614e3 @ 37 : 0d LdaUndefined
77 S> 0xf19437614e4 @ 38 : ab Return
Constant pool (size = 2)
Handler Table (size = 0)
0
1
2
短い。
-
r0
を0
にし、3
と比較。ここはループ判定とわかる。 -
0xf19437614ca
以下は、console.log(i)
の呼び出しということは先ほど見てきたletFunctions.push()
からすぐにわかる。 - その後
r0
の値を+1
して、ループの頭に戻っている。
すなわち、環境も作らず、PushContext
/PopContext
もない。変数i
はレジスタr0
を割り当てている。いちいち環境に格納するまでもない、ということである。このように最適化によって環境を作る処理が省略されている。
結論
クロージャがある場合はループの各回で環境が作られるが、必要ないところではちゃんと最適化されていることがわかった。
varだと?
クロージャを作る場合
[generated bytecode for function: varClosures (0x3b4588b60e31 <SharedFunctionInfo varClosures>)]
Parameter count 1
Register count 4
Frame size 32
0x3b4588b61626 @ 0 : 84 00 01 CreateFunctionContext [0], [1]
0x3b4588b61629 @ 3 : 16 fb PushContext r0
45 E> 0x3b4588b6162b @ 5 : a7 StackCheck
65 S> 0x3b4588b6162c @ 6 : 0b LdaZero
65 E> 0x3b4588b6162d @ 7 : 1d 04 StaCurrentContextSlot [4]
70 S> 0x3b4588b6162f @ 9 : 1a 04 LdaCurrentContextSlot [4]
0x3b4588b61631 @ 11 : 26 fa Star r1
0x3b4588b61633 @ 13 : 0c 03 LdaSmi [3]
70 E> 0x3b4588b61635 @ 15 : 69 fa 00 TestLessThan r1, [0]
0x3b4588b61638 @ 18 : 9a 25 JumpIfFalse [37] (0x3b4588b6165d @ 55)
52 E> 0x3b4588b6163a @ 20 : a7 StackCheck
87 S> 0x3b4588b6163b @ 21 : 19 fb 04 00 LdaImmutableContextSlot r0, [4], [0]
0x3b4588b6163f @ 25 : ac 01 ThrowReferenceErrorIfHole [1]
0x3b4588b61641 @ 27 : 26 f9 Star r2
100 E> 0x3b4588b61643 @ 29 : 28 f9 02 01 LdaNamedProperty r2, [2], [1]
0x3b4588b61647 @ 33 : 26 fa Star r1
0x3b4588b61649 @ 35 : 81 03 00 02 CreateClosure [3], [0], #2
0x3b4588b6164d @ 39 : 26 f8 Star r3
100 E> 0x3b4588b6164f @ 41 : 59 fa f9 f8 03 CallProperty1 r1, r2, r3, [3]
76 S> 0x3b4588b61654 @ 46 : 1a 04 LdaCurrentContextSlot [4]
0x3b4588b61656 @ 48 : 4c 05 Inc [5]
76 E> 0x3b4588b61658 @ 50 : 1d 04 StaCurrentContextSlot [4]
0x3b4588b6165a @ 52 : 8a 2b 00 JumpLoop [43], [0] (0x3b4588b6162f @ 9)
0x3b4588b6165d @ 55 : 0d LdaUndefined
132 S> 0x3b4588b6165e @ 56 : ab Return
Constant pool (size = 4)
Handler Table (size = 0)
3
3
3
-
CreateFunctionContext
が最初に実行され、環境はそれ以外作られない。var
は関数内スコープとなるので、直感的な予想とも合致する。
クロージャを作らない場合
今回は関数内で他にvar/let/constで宣言された変数とかがないため、letと同じ結果が得られる。
[generated bytecode for function: varFor (0x0d1a8bba0df1 <SharedFunctionInfo varFor>)]
Parameter count 1
Register count 3
Frame size 24
15 E> 0xd1a8bba14be @ 0 : a7 StackCheck
35 S> 0xd1a8bba14bf @ 1 : 0b LdaZero
0xd1a8bba14c0 @ 2 : 26 fb Star r0
40 S> 0xd1a8bba14c2 @ 4 : 0c 03 LdaSmi [3]
40 E> 0xd1a8bba14c4 @ 6 : 69 fb 00 TestLessThan r0, [0]
0xd1a8bba14c7 @ 9 : 9a 1c JumpIfFalse [28] (0xd1a8bba14e3 @ 37)
22 E> 0xd1a8bba14c9 @ 11 : a7 StackCheck
57 S> 0xd1a8bba14ca @ 12 : 13 00 01 LdaGlobal [0], [1]
0xd1a8bba14cd @ 15 : 26 f9 Star r2
65 E> 0xd1a8bba14cf @ 17 : 28 f9 01 03 LdaNamedProperty r2, [1], [3]
0xd1a8bba14d3 @ 21 : 26 fa Star r1
65 E> 0xd1a8bba14d5 @ 23 : 59 fa f9 fb 05 CallProperty1 r1, r2, r0, [5]
46 S> 0xd1a8bba14da @ 28 : 25 fb Ldar r0
0xd1a8bba14dc @ 30 : 4c 07 Inc [7]
0xd1a8bba14de @ 32 : 26 fb Star r0
0xd1a8bba14e0 @ 34 : 8a 1e 00 JumpLoop [30], [0] (0xd1a8bba14c2 @ 4)
0xd1a8bba14e3 @ 37 : 0d LdaUndefined
77 S> 0xd1a8bba14e4 @ 38 : ab Return
Constant pool (size = 2)
Handler Table (size = 0)
0
1
2
(おわり)