JavaScript Call Stack und mehr Magie





Anfang April wurde auf Habré der Artikel "JavaScript: Aufrufstapel und die Magie seiner Größe" veröffentlicht - der Autor kam zu dem Schluss, dass jeder Stapelrahmen Bytes belegt (72 + 8 * Anzahl lokaler Variablen): "Es stellt sich heraus, dass wir haben alles richtig gezählt und wir können behaupten, dass die Größe eines leeren ExecutionStack in Chrome 72 Bytes beträgt und die Größe des Callstacks knapp unter einem Megabyte liegt. Ausgezeichnete Arbeit!"



Zum Seeding - ändern wir den verwendeten Code leicht AxemaFr für Experimente:



{let i = 0;

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

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





Anstelle von 1 fügen wir jetzt bei jedem Schritt etwas mehr hinzu, und als Ergebnis erhalten wir anstelle von 13951 12556.000000000002 - als ob der Funktion eine lokale Variable hinzugefügt worden wäre!



Wiederholen wir die Fragen des Senior Frontend Developer AxemaFr: „Warum ist es so? Was hat sich geändert? Wie kann man verstehen, wenn man die Funktion betrachtet, wie oft sie rekursiv ausgeführt werden kann ?! "



Kochutensilien



In der Chrome-Befehlszeile können Sie Argumente an die JS-Engine übergeben. Insbesondere können Sie die Stapelgröße von 984 KB auf einen anderen Schlüssel ändern --js-flags=--stack-size=



.



Um herauszufinden, wie viel Stapel jede Funktion benötigt, hilft uns der --print-bytecode



bereits erwähnte Schlüssel . Es wurde nicht erwähnt, dass die Debug-Ausgabe an stdout gesendet wird, was Chrome unter Windows dummerweise nicht hat, da es als GUI-Anwendung kompiliert ist. Es ist einfach zu beheben: Erstellen Sie eine Kopie von chrome.exe, und korrigieren Sie in Ihrem bevorzugten Hex-Editor das Byte 0xD4



vom Wert 0x02



auf 0x03



(für diejenigen, die nicht mit dem Hex-Editor befreundet sind, hilft dieses Byte beim Beheben des Python-Skripts). Wenn Sie diesen Artikel jedoch gerade in Chrome lesen und nur die gepatchte Datei ausführen - sagen wir, Sie haben sie cui_chrome.exe genannt -, wird in einer vorhandenen Browserinstanz ein neues Fenster geöffnet und das Argument --js-flags



wird ignoriert. Um eine neue Instanz von Chrome zu starten, müssen Sie eine neue übergeben --user-data-dir



:

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





Ohne werden --print-bytecode-filter



Sie in kilometerlangen Bytecode-Dumps von Funktionen ertrinken, die in Chrome integriert sind.



Öffnen Sie nach dem Starten des Browsers die Entwicklerkonsole und geben Sie den verwendeten Code ein AxemaFr::



{let i = 0;

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

func()}
      
      





Bevor Sie die Eingabetaste drücken, wird im Konsolenfenster hinter Chrome ein Speicherauszug angezeigt:

[generierter Bytecode für Funktion: func (0x44db08635355 <SharedFunctionInfo func>)]
Parameteranzahl 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 Return
Konstanter Pool (Größe = 2)
Handler-Tabelle (Größe = 0)
Quellpositionstabelle (Größe = 12)


Wie ändert sich der Speicherauszug, wenn die Zeile i++;



durch ersetzt wird i += 1.00000000000001;



?

[generierter Bytecode für Funktion: func (0x44db0892d495 <SharedFunctionInfo func>)]
Parameteranzahl 1
Registeranzahl 2
Rahmengröße 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 Return
Konstanter Pool (Größe = 3)
Handler-Tabelle (Größe = 0)
Quellpositionstabelle (Größe = 12)


Lassen Sie uns nun herausfinden, was sich geändert hat und warum.



Beispiele erkunden



Alle V8-Opcodes werden unter github.com/v8/v8/blob/master/src/interpreter/interpreter-generator.cc beschrieben.

Der erste Speicherauszug wird folgendermaßen dekodiert:

LdaCurrentContextSlot [2]; a: = Kontext [2]
ThrowReferenceErrorIfHole [0]; if (a === undefined)
                                    ;; throw ("ReferenceError:% s ist nicht definiert", const [0])
Inc [0]; a ++
Stern r0; r0: = a
LdaCurrentContextSlot [2]; a: = Kontext [2]
ThrowReferenceErrorIfHole [0]; if (a === undefined)
                                    ;; throw ("ReferenceError:% s ist nicht definiert", 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


Das letzte Argument verschlüsselt Inc



und CallUndefinedReceiver0



legt den Feedback-Slot fest, in dem der Optimierer Statistiken über die verwendeten Typen sammelt. Dies hat keinen Einfluss auf die Semantik des Bytecodes, daher sind wir heute überhaupt nicht interessiert.



Unter dem Speicherauszug befindet sich ein Postskriptum: "Konstanter Pool (Größe = 2)" - und tatsächlich sehen wir, dass der Bytecode zwei Zeilen verwendet - "i"



und "func"



- zur Ersetzung in der Ausnahmemeldung, wenn Symbole mit solchen Namen undefiniert sind. Über dem Speicherauszug befindet sich ein Postskriptum: "Frame size 8" - entsprechend der Tatsache, dass die Funktion ein Interpreterregister verwendet ( r0



).



Der Stapelrahmen unserer Funktion besteht aus:



  • einzelnes Argument this



    ;
  • Absenderadressen;
  • die Anzahl der übergebenen Argumente ( arguments.length



    );
  • Verweise auf konstanten Pool mit verwendeten Zeichenfolgen;
  • Links zum Kontext mit lokalen Variablen;
  • drei weitere Zeiger, die der Motor benötigt; und endlich,
  • Platz für ein Register.


Insgesamt 9 * 8 = 72 Bytes als Signor AxemaFrund herausgefunden.



Von den sieben aufgelisteten Begriffen können sich theoretisch drei ändern - die Anzahl der Argumente, das Vorhandensein eines konstanten Pools und die Anzahl der Register. Was haben wir in der Variante mit 1.00000000000001 bekommen?



LdaCurrentContextSlot [2]; a: = Kontext [2]
ThrowReferenceErrorIfHole [0]; if (a === undefined)
                               ;; throw ("ReferenceError:% s ist nicht definiert", const [0])
Stern r0; r0: = a
LdaConstant [1]; a: = const [1]
Addiere r0, [0]; a + = r0
Stern r1; r1: = a
                               ;; ... weiter wie bisher


Erstens nahm die hinzugefügte Konstante im Konstantenpool den dritten Platz ein; Zweitens wurde ein weiteres Register benötigt, um es zu laden, sodass der Stapelrahmen der Funktion um 8 Bytes wuchs.



Wenn Sie in der Funktion keine benannten Symbole verwenden, können Sie auf den konstanten Pool verzichten. Auf github.com/v8/v8/blob/master/src/execution/frame-constants.h#L289 wurde das V8- Stapelrahmenformat beschrieben und angegeben, dass die Stapelrahmengröße um einen Zeiger reduziert wird, wenn der konstante Pool nicht verwendet wird . Wie können Sie sich dessen sicher sein? Auf den ersten Blick scheint es, dass eine Funktion, die keine benannten Symbole verwendet, nicht rekursiv sein kann. aber schauen Sie mal:



{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)


Das Ziel - "Konstanter Pool (Größe = 0)" - wurde erreicht; Der Stapelüberlauf tritt jedoch nach wie vor bei 13951-Aufrufen auf. Dies bedeutet, dass der Stapelrahmen der Funktion auch dann einen Zeiger darauf enthält, wenn der konstante Pool nicht verwendet wird.



Ist es möglich, eine kleinere Stapelrahmengröße als die berechnete zu erreichen AxemaFrMindestwert? - Ja, wenn innerhalb der Funktion kein Register verwendet wird:

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

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

chain()}
      
      





[generierter Bytecode für Funktion: func (0x44db08c34059 <SharedFunctionInfo func>)]
Parameteranzahl 1
Registeranzahl 0
Rahmengröße 0
   25 S> 000044DB08C34322 @ 0: 5d 02 00 CallUndefinedReceiver0 <dies>, [0]
         000044DB08C34325 @ 3: 0d LdaUndefined
   29 S> 000044DB08C34326 @ 4: ab Return
Konstanter Pool (Größe = 0)
Handler-Tabelle (Größe = 0)
Quellpositionstabelle (Größe = 6)


(In diesem Fall führt eine Kette von Aufrufen von 15051 bereits zu "RangeError: Maximale Aufrufstapelgröße überschritten".)



Somit ist die Schlussfolgerung des Unterzeichners AxemaFrdass „die Größe eines leeren ExecutionStack in Chrome 72 Byte ist“ hat erfolgreich widerlegt.



Vorhersagen verfeinern



Wir können argumentieren, dass die minimale Stack-Frame-Größe für eine JS-Funktion in Chrome 64 Byte beträgt . Dazu müssen Sie 8 Bytes für jeden deklarierten formalen Parameter hinzufügen, weitere 8 Bytes für jeden tatsächlichen Parameter, der die Anzahl der deklarierten überschreitet, und weitere 8 Bytes für jedes verwendete Register. Für jede lokale Variable wird ein Register zugewiesen, um Konstanten zu laden, auf Variablen aus einem externen Kontext zuzugreifen, tatsächliche Parameter während Aufrufen zu übergeben usw. Es ist kaum möglich, die genaue Anzahl der verwendeten Register aus dem Quellcode in JS zu bestimmen. Es ist anzumerken, dass der JS-Interpreter eine unbegrenzte Anzahl von Registern unterstützt - sie beziehen sich nicht auf die Register des Prozessors, auf dem der Interpreter ausgeführt wird.



Jetzt ist klar warum:
  • (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