Abhängigkeit der Codeleistung vom Kontext der Variablendeklaration in JavaScript


Ursprünglich war dieser Artikel als kleiner Benchmark für den eigenen Gebrauch konzipiert, und im Allgemeinen war nicht geplant, ein Artikel zu sein. Bei der Durchführung von Messungen tauchten jedoch einige interessante Funktionen bei der Implementierung der JavaScript- Architektur auf, die in einigen Fällen die Leistung des endgültigen Codes stark beeinträchtigen. Ich schlage vor, dass Sie sich mit den erzielten Ergebnissen vertraut machen und im Übrigen auch einige verwandte Themen untersuchen: für Schleifen, Umgebung (Ausführungskontext) und Blöcke.

Am Ende meines Artikels „Verwenden von let-Variablendeklarationen und -Funktionen der resultierenden JavaScript-Schließungen“ habe ich kurz auf den Leistungsvergleich von let- (LexicalDeclaration) und var- (VarDeclaredNames) Variablendeklarationen in Schleifen eingegangen . Zum Vergleich haben wir die Laufzeit des manuellen (ohne die Hilfe von Array.prototype.sort () ) Sortierens des Arrays verwendet. Eine der einfachsten Methoden ist das Sortieren nach Auswahl, da wir mit einer Array-Länge von 100.000 etwas mehr als 5 Milliarden haben. Iterationen in zwei Zyklen (extern und verschachtelt), und diese Zahl hätte am Ende eine angemessene Bewertung ermöglichen müssen.

Für var wurde die Ansicht sortiert:

for (var i = 0, len = arr.length; i < len-1; i++) { var min, mini = i; for (var j = i+1; j < len; j++) { if (arr[mini] > arr[j]) mini = j; } min = arr[mini]; arr[mini] = arr[i]; arr[i] = min; } //   Firefox: 9.082 . //   Chrome: 10.783 . 

Und für lassen Sie :

 for (let i = 0, len = arr.length; i < len-1; i++) { let min, mini = i; for (let j = i+1; j < len; j++) { if (arr[mini] > arr[j]) mini = j; } min = arr[mini]; arr[mini] = arr[i]; arr[i] = min; } //   Firefox: 5.261 . //   Chrome: 5.391 . 

Wenn man diese Zahlen sieht, kann man eindeutig argumentieren, dass Anzeigen die Geschwindigkeitsvariante vollständig übertreffen. Zusätzlich zu dieser Schlussfolgerung blieb jedoch die Frage offen: Was wird passieren, wenn wir Deklarationen außerhalb von for- Schleifen platzieren?

Bevor Sie dies tun, müssen Sie sich jedoch eingehender mit der Arbeit der for- Schleife befassen, die sich an der aktuellen Spezifikation von ECMAScript 2019 (ECMA-262) orientiert :

 13.7.4.7Runtime Semantics: LabelledEvaluation With parameter labelSet. IterationStatement':'for(Expression;Expression;Expression)Statement 1. If the first Expression is present, then a. Let exprRef be the result of evaluating the first Expression. b. Perform ? GetValue(exprRef). 2. Return ? ForBodyEvaluation(the second Expression, the third Expression, Statement, « », labelSet). IterationStatement':'for(varVariableDeclarationList;Expression;Expression)Statement 1. Let varDcl be the result of evaluating VariableDeclarationList. 2. ReturnIfAbrupt(varDcl). 3. Return ? ForBodyEvaluation(the first Expression, the second Expression, Statement, « », labelSet). IterationStatement':'for(LexicalDeclarationExpression;Expression)Statement 1. Let oldEnv be the running execution context's LexicalEnvironment. 2. Let loopEnv be NewDeclarativeEnvironment(oldEnv). 3. Let loopEnvRec be loopEnv's EnvironmentRecord. 4. Let isConst be the result of performing IsConstantDeclaration of LexicalDeclaration. 5. Let boundNames be the BoundNames of LexicalDeclaration. 6. For each element dn of boundNames, do a. If isConst is true, then i. Perform ! loopEnvRec.CreateImmutableBinding(dn, true). b. Else, i. Perform ! loopEnvRec.CreateMutableBinding(dn, false). 7. Set the running execution context's LexicalEnvironment to loopEnv. 8. Let forDcl be the result of evaluating LexicalDeclaration. 9. If forDcl is an abrupt completion, then a. Set the running execution context's LexicalEnvironment to oldEnv. b. Return Completion(forDcl). 10. If isConst is false, let perIterationLets be boundNames; otherwise let perIterationLets be « ». 11. Let bodyResult be ForBodyEvaluation(the first Expression, the second Expression, Statement, perIterationLets, labelSet). 12. Set the running execution context's LexicalEnvironment to oldEnv. 13. Return Completion(bodyResult). 
Hinweis: Doppelpunkte nach IterationStatements werden in der Quelle nicht von Apostrophen eingerahmt. Sie werden hier hinzugefügt, damit keine automatische Formatierung die Lesbarkeit des Textes beeinträchtigt.

Wie wir sehen, gibt es hier drei Möglichkeiten zum Aufrufen und weiteren Arbeiten der for- Schleife:
  • mit for (Ausdruck; Ausdruck; Ausdruck) Anweisung
    ForBodyEvaluation (der zweite Ausdruck, der dritte Ausdruck, Anweisung, "", labelSet) .
  • mit for (varVariableDeclarationList; Expression; Expression) -Anweisung
    ForBodyEvaluation (der erste Ausdruck, der zweite Ausdruck, Anweisung, "", labelSet).
  • at for (LexicalDeclarationExpression; Expression) Anweisung
    ForBodyEvaluation (der erste Ausdruck, der zweite Ausdruck, Anweisung, perIterationLets, labelSet)

In der letzten, dritten Variante ist der vierte Parameter im Gegensatz zu den ersten beiden nicht leer - perIterationLets - dies sind tatsächlich die gleichen Let- Deklarationen im ersten Parameter, der an die for- Schleife übergeben wird. Sie sind in Absatz 10 angegeben:
- Wenn isConst false ist , lassen Sie perIterationLets boundNames sein. Andernfalls sei perIterationLets "".
Wenn eine Konstante an for übergeben wurde , jedoch keine Variable, wird der Parameter perIterationLets leer.

Bei der dritten Option muss auch auf Absatz 2 geachtet werden:
- Sei loopEnv NewDeclarativeEnvironment (oldEnv).

 8.1.2.2NewDeclarativeEnvironment ( E ) When the abstract operation NewDeclarativeEnvironment is called with a Lexical Environment as argument E the following steps are performed: 1. Let env be a new Lexical Environment. 2. Let envRec be a new declarative Environment Record containing no bindings. 3. Set env's EnvironmentRecord to envRec. 4. Set the outer lexical environment reference of env to E. 5. Return env. 

Hier wird als Parameter E die Umgebung verwendet, aus der die for- Schleife aufgerufen wurde (global, eine beliebige Funktion usw.), und es wird eine neue Umgebung erstellt, um die for- Schleife unter Bezugnahme auf die externe Umgebung auszuführen, die sie erstellt hat (Punkt 4). Wir sind an dieser Tatsache interessiert, da die Umgebung ein Kontext der Ausführung ist.

Und wir erinnern uns, dass let- und const- Variablendeklarationen kontextuell an den Block gebunden sind, in dem sie deklariert sind.

 13.2.14Runtime Semantics: BlockDeclarationInstantiation ( code, env ) Note When a Block or CaseBlock is evaluated a new declarative Environment Record is created and bindings for each block scoped variable, constant, function, or class declared in the block are instantiated in the Environment Record. BlockDeclarationInstantiation is performed as follows using arguments code and env. code is the Parse Node corresponding to the body of the block. env is the Lexical Environment in which bindings are to be created. 1. Let envRec be env's EnvironmentRecord. 2. Assert: envRec is a declarative Environment Record. 3. Let declarations be the LexicallyScopedDeclarations of code. 4. For each element d in declarations, do a. For each element dn of the BoundNames of d, do i. If IsConstantDeclaration of d is true, then 1. Perform ! envRec.CreateImmutableBinding(dn, true). ii. Else, 1. Perform ! envRec.CreateMutableBinding(dn, false). b. If d is a FunctionDeclaration, a GeneratorDeclaration, an AsyncFunctionDeclaration, or an AsyncGeneratorDeclaration, then i. Let fn be the sole element of the BoundNames of d. ii. Let fo be the result of performing InstantiateFunctionObject for d with argument env. iii. Perform envRec.InitializeBinding(fn, fo). 

Hinweis: Da es in den ersten beiden Varianten des Aufrufs der for- Schleife keine solchen Deklarationen gab, war es nicht erforderlich, eine neue Umgebung für sie zu erstellen.

Wir gehen weiter und überlegen, was ForBodyEvaluation ist :

 13.7.4.8Runtime Semantics: ForBodyEvaluation ( test, increment, stmt, perIterationBindings, labelSet ) The abstract operation ForBodyEvaluation with arguments test, increment, stmt, perIterationBindings, and labelSet is performed as follows: 1. Let V be undefined. 2. Perform ? CreatePerIterationEnvironment(perIterationBindings). 3. Repeat, a. If test is not [empty], then i. Let testRef be the result of evaluating test. ii. Let testValue be ? GetValue(testRef). iii. If ToBoolean(testValue) is false, return NormalCompletion(V). b. Let result be the result of evaluating stmt. c. If LoopContinues(result, labelSet) is false, return Completion(UpdateEmpty(result, V)). d. If result.[[Value]] is not empty, set V to result.[[Value]]. e. Perform ? CreatePerIterationEnvironment(perIterationBindings). f. If increment is not [empty], then i. Let incRef be the result of evaluating increment. ii. Perform ? GetValue(incRef). 

Worauf Sie zuerst achten sollten:
  • Beschreibung der eingehenden Parameter:
    • test : Ausdruck vor der nächsten Iteration des Schleifenkörpers auf Wahrheit überprüft (zum Beispiel: i <len );
    • Inkrement : Ausdruck, der zu Beginn jeder neuen Iteration ausgewertet wird (mit Ausnahme der ersten) (zum Beispiel: i ++ );
    • stmt : Schleifenkörper
    • perIterationBindings : Variablen, die mit let im ersten for- Parameter deklariert wurden (zum Beispiel: let i = 0 || let i || let i, j );
    • labelSet : Label der Schleife;
  • Punkt 2: Wenn hier der nicht leere Parameter perIterationBindings übergeben wird , wird eine zweite Umgebung erstellt, um den ersten Durchlauf der Schleife durchzuführen.
  • Absatz 3.a: Überprüfung auf eine bestimmte Bedingung für die Fortsetzung der Ausführung des Zyklus;
  • Abschnitt 3.b: Ausführung des Zykluskörpers;
  • Punkt 3.e: Schaffung einer neuen Umgebung.

Nun, und direkt der Algorithmus zum Erstellen interner Umgebungen der for- Schleife:

 13.7.4.9Runtime Semantics: CreatePerIterationEnvironment ( perIterationBindings ) 1. The abstract operation CreatePerIterationEnvironment with argument perIterationBindings is performed as follows: 1. If perIterationBindings has any elements, then a. Let lastIterationEnv be the running execution context's LexicalEnvironment. b. Let lastIterationEnvRec be lastIterationEnv's EnvironmentRecord. c. Let outer be lastIterationEnv's outer environment reference. d. Assert: outer is not null. e. Let thisIterationEnv be NewDeclarativeEnvironment(outer). f. Let thisIterationEnvRec be thisIterationEnv's EnvironmentRecord. g. For each element bn of perIterationBindings, do i. Perform ! thisIterationEnvRec.CreateMutableBinding(bn, false). ii. Let lastValue be ? lastIterationEnvRec.GetBindingValue(bn, true). iii. Perform thisIterationEnvRec.InitializeBinding(bn, lastValue). h. Set the running execution context's LexicalEnvironment to thisIterationEnv. 2. Return undefined. 

Wie wir sehen können, prüft der erste Absatz das Vorhandensein von Elementen im übergebenen Parameter, und Absatz 1 wird nur ausgeführt, wenn es Ansagen gibt. Alle neuen Umgebungen werden unter Bezugnahme auf denselben externen Kontext erstellt und verwenden die neuesten Werte aus der vorherigen Iteration (vorherige Arbeitsumgebung) als neue Bindungen für let- Variablen.

Betrachten Sie als Beispiel einen ähnlichen Ausdruck:

 let arr = []; for (let i = 0; i < 3; i++) { arr.push(i); } console.log(arr); // Array(3) [ 0, 1, 2 ] 

Und so kann es zerlegt werden, ohne es zu verwenden (mit einem gewissen Maß an Konventionalität):

 let arr = []; //    { let i = 0; //     for } //   ,   { let i = 0; //    i    if (i < 3) arr.push(i); } //    { let i = 0; //    i    i++; if (i < 3) arr.push(i); } //    { let i = 1; //    i    i++; if (i < 3) arr.push(i); } //    { let i = 2; //    i    i++; if (i < 3) arr.push(i); } console.log(arr); // Array(3) [ 0, 1, 2 ] 

Tatsächlich kommen wir zu dem Schluss, dass wir für jeden Kontext, und hier haben wir fünf davon, neue Bindungen für let- Variablen erstellen, die als erster Parameter in for deklariert sind (wichtig: Dies gilt nicht für let- Deklarationen direkt im Hauptteil der Schleife).

So sieht diese Schleife beispielsweise aus, wenn var verwendet wird, wenn keine zusätzlichen Bindungen vorhanden sind:

 let arr2 = []; var i = 0; if (i < 3) arr.push(i); i++; if (i < 3) arr.push(i); i++; if (i < 3) arr.push(i); i++; if (i < 3) arr.push(i); console.log(arr); // Array(3) [ 0, 1, 2 ] 

Und wir können zu dem scheinbar logischen Schluss kommen, dass wir, wenn während der Ausführung unserer Schleifen keine separaten Bindungen für jede Iteration erstellt werden müssen ( mehr über Situationen, in denen dies im Gegenteil sinnvoll sein kann ), vorher die Deklaration inkrementeller Variablen vornehmen sollten mit einer for- Schleife, die uns vor dem Erstellen und Löschen einer großen Anzahl von Kontexten bewahren und theoretisch die Leistung verbessern soll.

Versuchen wir dies anhand der gleichen Sortierung eines Arrays von 100.000 Elementen als Beispiel. Aus Gründen der Schönheit definieren wir auch alle anderen Variablen zuvor für :

 let i, j, min, mini, len = arr.length; for (i = 0; i < len-1; i++) { mini = i; for (j = i+1; j < len; j++) { if (arr[mini] > arr[j]) mini = j; } min = arr[mini]; arr[mini] = arr[i]; arr[i] = min; } //   Firefox: 34.246 . //   Chrome: 10.803 . 

Unerwartetes Ergebnis ... Genau das Gegenteil von dem, was erwartet wurde, um genau zu sein. Besonders auffällig ist der Firefox- Drawdown in diesem Test.

Ok Dies hat nicht funktioniert. Lassen Sie uns die Deklaration der Variablen i und j auf die Parameter der entsprechenden Zyklen zurückführen:

 let min, mini, len = arr.length; for (let i = 0; i < len-1; i++) { mini = i; for (let j = i+1; j < len; j++) { if (arr[mini] > arr[j]) mini = j; } min = arr[mini]; arr[mini] = arr[i]; arr[i] = min; } //   Firefox: 6.575 . //   Chrome: 6.749 . 

Hm. Technisch gesehen scheint der einzige Unterschied zwischen dem letzten Beispiel und dem Beispiel am Anfang des Artikels die gemachten Deklarationen der Variablen min, mini und len außerhalb der for- Schleife zu sein, und obwohl der Unterschied immer noch kontextbezogen ist, ist er für uns jetzt nicht von besonderem Interesse und darüber hinaus Wir haben die Notwendigkeit beseitigt, diese Variablen im Körper des Zyklus der oberen Ebene 99.999 Mal zu deklarieren, was theoretisch wiederum die Produktivität erhöhen sollte, anstatt sie um mehr als eine Sekunde zu verringern.

Das heißt, es stellt sich heraus, dass die Arbeit mit Variablen, die im Parameter oder Hauptteil der for- Schleife deklariert sind, viel schneller erfolgt als außerhalb.

Wir schienen jedoch in der Spezifikation für die for- Schleife keine "Turbo" -Anweisungen zu sehen, die uns zu einer solchen Idee führen könnten. Daher sind es nicht die Besonderheiten der Arbeit der for- Schleife, sondern etwas anderes ... Zum Beispiel die Merkmale von let- Deklarationen: Was ist das Hauptmerkmal, das let von var unterscheidet ? Blockausführungskontext! In unseren letzten beiden Beispielen haben wir Anzeigen außerhalb des Blocks verwendet. Was aber, wenn wir diese Deklarationen nicht auf for verschieben, sondern nur einen separaten Block für sie auswählen?

 { let i, j, min, mini, len = arr.length; for (i = 0; i < len-1; i++) { mini = i; for (j = i+1; j < len; j++) { if (arr[mini] > arr[j]) mini = j; } min = arr[mini]; arr[mini] = arr[i]; arr[i] = min; } } //   Firefox: 5.262 . //   Chrome: 5.405 . 

Voila! Es stellte sich heraus, dass der Haken darin bestand, dass Ankündigungen in einem globalen Kontext stattfanden, und sobald wir ihnen einen separaten Block zugewiesen hatten, verschwanden alle Probleme genau dort.

Und hier wäre es schön, sich an eine andere, leicht unverdient verfluchte Art der Deklaration von Variablen zu erinnern - var .

Im Beispiel am Anfang des Artikels zeigte die Sortierzeit mit var im Vergleich zu let ein äußerst bedauerliches Ergebnis. Wenn Sie sich dieses Beispiel genauer ansehen, werden Sie möglicherweise feststellen, dass der tatsächliche Kontext der Variablen global war, da var keine Variablenblockbindungen hatte. Und am Beispiel von let haben wir bereits herausgefunden, wie sich dies auf die Leistung auswirken kann (und was bei Verwendung von let typisch ist, stellte sich heraus, dass der Geschwindigkeitsabfall stärker war als im Fall von var , insbesondere in Firefox ). Aus Fairnessgründen werden wir daher ein Beispiel mit var ausführen, um einen neuen Kontext für Variablen zu erstellen:

 function test() { var i, j, min, mini, len = arr.length; for (i = 0; i < len-1; i++) { mini = i; for (j = i+1; j < len; j++) { if (arr[mini] > arr[j]) mini = j; } min = arr[mini]; arr[mini] = arr[i]; arr[i] = min; } } test(); //   Firefox: 5.255 . //   Chrome: 5.411 . 

Und wir haben das Ergebnis fast identisch mit dem bei der Verwendung von let erhalten .

Überprüfen Sie abschließend, ob die Verlangsamung auftritt, indem Sie die globale Variable lesen, ohne ihren Wert zu ändern.

lass

 let len = arr.length; for (let i = 0; i < len-1; i++) { let min, mini = i; for (let j = i+1; j < len; j++) { if (arr[mini] > arr[j]) mini = j; } min = arr[mini]; arr[mini] = arr[i]; arr[i] = min; } //   Firefox: 5.262 . //   Chrome: 5.391 . 

var

 var len = arr.length; function test() { var i, j, min, mini; for (i = 0; i < len-1; i++) { mini = i; for (j = i+1; j < len; j++) { if (arr[mini] > arr[j]) mini = j; } min = arr[mini]; arr[mini] = arr[i]; arr[i] = min; } } test(); //   Firefox: 5.258 . //   Chrome: 5.439 . 

Die Ergebnisse zeigen, dass das Lesen der globalen Variablen die Ausführungszeit nicht beeinflusst hat.

Zusammenfassend


  1. Das Ändern globaler Variablen ist viel langsamer als das Ändern lokaler Variablen. In Anbetracht dessen können Sie den Code in geeigneten Situationen optimieren, indem Sie einen separaten Block oder eine separate Funktion erstellen, einschließlich zum Deklarieren von Variablen, anstatt einen Teil des Codes in einem globalen Kontext auszuführen. Ja, in fast jedem Lehrbuch finden Sie Empfehlungen, wie Sie so wenig globale Bindungen wie möglich herstellen können. In der Regel wird jedoch nur eine Verstopfung des globalen Namespace als Grund angegeben und kein Wort über mögliche Leistungsprobleme.
  2. Trotz der Tatsache, dass die Ausführung von Schleifen mit einer let- Deklaration im ersten for- Parameter eine große Anzahl von Umgebungen erzeugt, hat dies fast keine Auswirkungen auf die Leistung, im Gegensatz zu Situationen, in denen wir solche Deklarationen außerhalb des Blocks ausführen. Dennoch sollte die Möglichkeit exotischer Situationen nicht ausgeschlossen werden, in denen dieser Faktor die Produktivität stärker beeinflusst.
  3. Die Leistung von var- Variablen ist der von let- Variablen immer noch nicht unterlegen, überschreitet sie jedoch nicht (wiederum im allgemeinen Fall), was uns zu der nächsten Schlussfolgerung führt, dass es keinen Grund gibt, var- Deklarationen zu verwenden, außer aus Kompatibilitätsgründen. Wenn Sie jedoch globale Variablen mit Änderungen ihrer Werte bearbeiten müssen, ist die var- Variante hinsichtlich der Leistung vorzuziehen (zumindest für den Moment, wenn insbesondere davon ausgegangen wird, dass das Skript auch auf der Gecko-Engine ausgeführt werden kann).

Referenzen


ECMAScript 2019 (ECMA-262)
Verwenden von let-Deklarationen von Variablen und Features der resultierenden Abschlüsse in JavaScript

Source: https://habr.com/ru/post/de469523/


All Articles