JavaScriptの呼び出しスタックとその他の魔法





初期の4月には、記事「JavaScriptの:コールスタックとその大きさの魔法」はHabréに掲載されていました -その作者は、各スタックフレームは、(72 + 8 *番号of_local_variables)を占めているという結論に来たバイト: 「それは私達ことが判明すべてが正しくカウントされ、Chromeの空のExecutionStackのサイズは72バイトであり、コールスタックのサイズは1メガバイトをわずかに下回ると断言できます。よくやった! "



シードの場合-使用するコードを少し変更しましょう AxemaFr 実験用:



{let i = 0;

const func = () => {
  i += 1.00000000000001;
  func();
};

try {
  func();
} catch (e) {
  console.log(i);
}}
      
      





1の代わりに、各ステップでもう少し追加します。その結果、13951の代わりに、12556.000000000002を取得します-ローカル変数が関数に追加されたかのように!



シニアフロントエンド開発者からの質問を繰り返しましょう AxemaFr「なぜそうなのですか?何が変わったの?関数を見て、何回再帰的に実行できるかを理解する方法は?!」



調理器具



Chromeコマンドラインでは、JSエンジンに引数を渡すことができます。特に、スタックサイズを984KBから他の任意のキーに変更できます --js-flags=--stack-size=







各機能が必要とどのくらいのスタックを理解するには、キー--print-bytecode



すでに 述べたが、私たちを助けます 。デバッグ出力がstdoutに送信されることについては言及されていませんでしたが、GUIアプリケーションとしてコンパイルされているため、WindowsでのChromeにはありません。それは修正するのは簡単です:chrome.exeのコピーを作成し、お好みのバイナリエディタでバイトを補正 0xD4



値から 0x02



0x03



バイナリエディタと友達ではない人のために(、このバイトは解決するのに役立ちます のPythonスクリプトを)。ただし、現在Chromeでこの記事を読んでいて、パッチを適用したファイルを実行した場合(たとえば、cui_chrome.exeという名前を付けた場合)、既存のブラウザーインスタンスで新しいウィンドウが開き、引数 --js-flags



は無視されます。Chromeの新しいインスタンスを開始するには、新しいインスタンスを渡す必要があります --user-data-dir





cui_chrome.exe --no-sandbox --js-flags="--print-bytecode --print-bytecode-filter=func" --user-data-dir=\Windows\Temp





これがないと、 --print-bytecode-filter



Chromeに組み込まれている関数のキロメートルバイトコードダンプに溺れてしまいます。



ブラウザを起動した後、開発者コンソールを開き、使用するコードを入力します AxemaFr



{let i = 0;

const func = () => {
  i++;
  func();
};

func()}
      
      





Enterキーを押す前に、Chromeの背後にあるコンソールウィンドウにダンプが表示されます。

[関数用に生成されたバイトコード:func(0x44db08635355 <SharedFunctionInfo func>)]
パラメータカウント1
Register count 1
Frame size 8
   36 S> 000044DB086355EE @    0 : 1a 02             LdaCurrentContextSlot [2]
         000044DB086355F0 @    2 : ac 00             ThrowReferenceErrorIfHole [0]
         000044DB086355F2 @    4 : 4d 00             Inc [0]
         000044DB086355F4 @    6 : 26 fa             Star r0
         000044DB086355F6 @    8 : 1a 02             LdaCurrentContextSlot [2]
   37 E> 000044DB086355F8 @   10 : ac 00             ThrowReferenceErrorIfHole [0]
         000044DB086355FA @   12 : 25 fa             Ldar r0
         000044DB086355FC @   14 : 1d 02             StaCurrentContextSlot [2]
   44 S> 000044DB086355FE @   16 : 1b 03             LdaImmutableCurrentContextSlot [3]
         000044DB08635600 @   18 : ac 01             ThrowReferenceErrorIfHole [1]
         000044DB08635602 @   20 : 26 fa             Star r0
   44 E> 000044DB08635604 @ 22:5d fa 01 CallUndefinedReceiver0 r0、[1]
         000044DB08635607 @ 25:0d LdaUndefined
   52 S> 000044DB08635608 @ 26:abリターン
定数プール(サイズ= 2)
ハンドラーテーブル(サイズ= 0)
ソース位置テーブル(サイズ= 12)


行がにi++;



置き換えられた場合、ダンプはどのように変化 i += 1.00000000000001;



ますか?

[関数用に生成されたバイトコード:func(0x44db0892d495 <SharedFunctionInfo func>)]
パラメータカウント1
登録数2
フレームサイズ16
   36 S> 000044DB0892D742 @ 0:1a 02 LdaCurrentContextSlot [2]
         000044DB0892D744 @ 2:ac 00 ThrowReferenceErrorIfHole [0]
         000044DB0892D746 @ 4:26 fa Star r0
         000044DB0892D748 @ 6:12 01 LdaConstant [1]
         000044DB0892D74A @    8 : 35 fa 00          Add r0, [0]
         000044DB0892D74D @   11 : 26 f9             Star r1
         000044DB0892D74F @   13 : 1a 02             LdaCurrentContextSlot [2]
   37 E> 000044DB0892D751 @   15 : ac 00             ThrowReferenceErrorIfHole [0]
         000044DB0892D753 @   17 : 25 f9             Ldar r1
         000044DB0892D755 @   19 : 1d 02             StaCurrentContextSlot [2]
   60 S> 000044DB0892D757 @   21 : 1b 03             LdaImmutableCurrentContextSlot [3]
         000044DB0892D759 @   23 : ac 02             ThrowReferenceErrorIfHole [2]
         000044DB0892D75B @   25 : 26 fa             Star r0
   60 E> 000044DB0892D75D @   27 : 5d fa 01          CallUndefinedReceiver0 r0, [1]
         000044DB0892D760 @   30 : 0d                LdaUndefined
   68 S> 000044DB0892D761 @ 31:abリターン
定数プール(サイズ= 3)
ハンドラーテーブル(サイズ= 0)
ソース位置テーブル(サイズ= 12)


それでは、何が変わったのか、そしてその理由を理解しましょう。



例を探る



すべてのV8オペコードはgithub.com/v8/v8/blob/master/src/interpreter/interpreter-generator.ccで 説明されてい ます

。最初のダンプは次のようにデコードされます。

LdaCurrentContextSlot [2]; a:=コンテキスト[2]
ThrowReferenceErrorIfHole [0]; if(a === undefined)
                                    ; throw( "ReferenceError:%s is not defined"、const [0])
Inc [0]; ++
スターr0; r0:= a
LdaCurrentContextSlot [2]; a:=コンテキスト[2]
ThrowReferenceErrorIfHole [0]; if(a === undefined)
                                    ; throw( "ReferenceError:%s is not defined"、const [0])
Ldar r0; a:= r0
StaCurrentContextSlot [2]           ; context[2] := a
LdaImmutableCurrentContextSlot [3]  ; a := context[3]
ThrowReferenceErrorIfHole [1]       ; if (a === undefined)
                                    ;   throw("ReferenceError: %s is not defined", const[1])
Star r0                             ; r0 := a
CallUndefinedReceiver0 r0, [1]      ; r0()
LdaUndefined                        ; a := undefined
Return


最後の引数は、オプティマイザーが使用されたタイプに関する統計を収集するフィードバックスロットをオペコード Inc



して CallUndefinedReceiver0



設定します。これはバイトコードのセマンティクスには影響しないため、今日はまったく関心がありません。



「コンスタントプール(サイズ= 2)」 -と確かに私たちは、バイトコードは、2つの行を使用していることがわかり- :ダンプの下にありPostScriptはある "i"



"func"



、そのような名前のシンボルが未定義されたときに例外メッセージでの置換のために- 。ダンプの上に「フレームサイズ8」という追記があります。これは、関数が1つのインタープリターレジスタ(r0



)を使用しているため です。



関数のスタックフレームは次のもので構成されています。



  • 単一の引数this



    ;
  • リターンアドレス;
  • 渡された引数の数(arguments.length



    );
  • 使用された文字列を含む定数プールへの参照。
  • ローカル変数を使用したコンテキストへのリンク。
  • エンジンに必要なさらに3つのポインター。そして最後に
  • 1つのレジスタ用のスペース。


署名者として合計9 * 8 = 72バイト AxemaFrそして理解した。



リストされている7つの用語のうち、理論的には3つが変更される可能性があります。引数の数、定数プールの存在、およびレジスタの数です。1.00000000000001のバリアントで何が得られましたか?



LdaCurrentContextSlot [2]; a:=コンテキスト[2]
ThrowReferenceErrorIfHole [0]; if(a === undefined)
                               ; throw( "ReferenceError:%s is not defined"、const [0])
スターr0; r0:= a
LdaConstant [1]; a:= const [1]
r0、[0]を追加します。a + = r0
スターr1; r1:= a
                               ; ...さらに前と同じ


まず、追加された定数が定数プールで3位になりました。次に、それをロードするためにもう1つのレジスタが必要だったため、関数のスタックフレームは8バイト増加しました。



関数で名前付きシンボルを使用しない場合は、定数プールなしで使用できます。github.com/v8/v8/blob/master/src/execution/frame-constants.h#L289 V8スタックフレームのフォーマットを説明した定数プールが使用されていない場合、スタック・フレーム・サイズは、1つのポインタによって低減されると述べ。どうすればこれを確信できますか?一見すると、名前付きシンボルを使用しない関数は再帰的ではないように見えます。しかし、見てください:



{let i = 0;

function func() {
  this()();
};

const helper = () => (i++, func.bind(helper));

try {
  helper()();
} catch (e) {
  console.log(i);
}}
      
      





[generated bytecode for function: func (0x44db0878e575 <SharedFunctionInfo func>)]
Parameter count 1
Register count 1
Frame size 8
   37 S> 000044DB0878E8DA @    0 : 5e 02 02 00       CallUndefinedReceiver1 <this>, <this>, [0]
         000044DB0878E8DE @    4 : 26 fa             Star r0
   43 E> 000044DB0878E8E0 @    6 : 5d fa 02          CallUndefinedReceiver0 r0, [2]
         000044DB0878E8E3 @    9 : 0d                LdaUndefined
   47 S> 000044DB0878E8E4 @   10 : ab                Return
Constant pool (size = 0)
Handler Table (size = 0)
Source Position Table (size = 8)


目標-「一定のプール(サイズ= 0)」-は達成されました。ただし、スタックオーバーフローは、以前と同様に、13951回の呼び出しで発生します。これは、定数プールが使用されていない場合でも、関数のスタックフレームにそれへのポインターが含まれていることを意味します。



計算されたよりも小さいスタックフレームサイズを達成することは可能ですか? AxemaFr最小値?-はい、関数内でレジスタが使用されていない場合:

{function func() {
  this();
};

let chain = ()=>null;
for(let i=0; i<15050; i++)
  chain = func.bind(chain);

chain()}
      
      





[関数用に生成されたバイトコード:func(0x44db08c34059 <SharedFunctionInfo func>)]
パラメータカウント1
レジスタ数0
フレームサイズ0
   25 S> 000044DB08C34322 @ 0:5d 02 00 CallUndefinedReceiver0 <this>、[0]
         000044DB08C34325 @ 3:0d LdaUndefined
   29 S> 000044DB08C34326 @ 4:abリターン
定数プール(サイズ= 0)
ハンドラーテーブル(サイズ= 0)
ソース位置テーブル(サイズ= 6)


(この場合、15051からの一連の呼び出しは、すでに「RangeError:最大呼び出しスタックサイズを超えました」につながります。)



したがって、署名者 の結論 AxemaFr「Chromeで空ExecutionStackのサイズは72バイトである」い、正常論破されて。



予測の精緻化



ChromeのJS関数の最小スタックフレームサイズは64バイトであると言え ます。これに、宣言された仮パラメーターごとに8バイト、宣言されたパラメーターの数を超える実際のパラメーターごとにさらに8バイト、および使用されるレジスタごとにさらに8バイトを追加する必要があります。レジスタは、各ローカル変数、定数のロード、外部コンテキストからの変数へのアクセス、呼び出し中に実際のパラメータを渡すためなどに割り当てられます。JSのソースコードから使用済みレジスタの正確な数を特定することはほとんど不可能です。JSインタープリターは無制限の数のレジスターをサポートしていることに注意してください。これらは、インタープリターが実行されるプロセッサーのレジスターとは関係ありません。



これで、理由は明らかです。
  • (func = (x) => { i++; func(); };



    ) , ;
  • (func = () => { i++; func(1); };



    ) , — :
    [generated bytecode for function: func (0x44db08e12da1 <SharedFunctionInfo func>)]
    Parameter count 1
    Register count 2
    Frame size 16
       34 S> 000044DB08E12FE2 @    0 : 1a 02             LdaCurrentContextSlot [2]
             000044DB08E12FE4 @    2 : ac 00             ThrowReferenceErrorIfHole [0]
             000044DB08E12FE6 @    4 : 4d 00             Inc [0]
             000044DB08E12FE8 @    6 : 26 fa             Star r0
             000044DB08E12FEA @    8 : 1a 02             LdaCurrentContextSlot [2]
       35 E> 000044DB08E12FEC @   10 : ac 00             ThrowReferenceErrorIfHole [0]
             000044DB08E12FEE @   12 : 25 fa             Ldar r0
             000044DB08E12FF0 @   14 : 1d 02             StaCurrentContextSlot [2]
       39 S> 000044DB08E12FF2 @   16 : 1b 03             LdaImmutableCurrentContextSlot [3]
             000044DB08E12FF4 @   18 : ac 01             ThrowReferenceErrorIfHole [1]
             000044DB08E12FF6 @   20 : 26 fa             Star r0
             000044DB08E12FF8 @   22 : 0c 01             LdaSmi [1]
             000044DB08E12FFA @   24 : 26 f9             Star r1
       39 E> 000044DB08E12FFC @   26 : 5e fa f9 01       CallUndefinedReceiver1 r0, r1, [1]
             000044DB08E13000 @   30 : 0d                LdaUndefined
       48 S> 000044DB08E13001 @   31 : ab                Return
    Constant pool (size = 2)
    Handler Table (size = 0)
    Source Position Table (size = 12)
  • 1.00000000000001 — r1



    , .







All Articles