Heim  >  Artikel  >  Web-Frontend  >  Verstehen Sie die Grundprinzipien von JavaScript-Engines

Verstehen Sie die Grundprinzipien von JavaScript-Engines

coldplay.xixi
coldplay.xixinach vorne
2020-10-10 17:28:592189Durchsuche

Als Entwickler der Spalte „JavaScript“ hilft Ihnen ein tiefes Verständnis der Funktionsweise der JavaScript-Engine, die Leistungsmerkmale Ihres Codes zu verstehen. In diesem Artikel werden einige wichtige Grundlagen behandelt, die allen JavaScript-Engines gemeinsam sind, nicht nur V8. JavaScript-Engine-Workflow

Alles beginnt mit dem JavaScript-Code, den Sie schreiben. Die JavaScript-Engine analysiert den Quellcode und wandelt ihn in einen abstrakten Syntaxbaum (AST) um. Basierend auf dem AST kann der Interpreter mit der Arbeit beginnen und Bytecode generieren. An diesem Punkt beginnt die Engine tatsächlich mit der Ausführung von JavaScript-Code.

Um die Ausführung zu beschleunigen, kann der Bytecode zusammen mit den Profilierungsdaten an den optimierenden Compiler gesendet werden. Ein optimierender Compiler trifft bestimmte Annahmen auf der Grundlage verfügbarer Profilierungsdaten und generiert dann hochoptimierten Maschinencode.

Verstehen Sie die Grundprinzipien von JavaScript-EnginesWenn sich eine Annahme irgendwann als falsch erweist, bricht der optimierende Compiler die Optimierung ab und kehrt zur Interpreterstufe zurück.

Interpreter-/Compiler-Workflow in JavaScript-Engines

Schauen wir uns nun den Teil des Prozesses an, der tatsächlich JavaScript-Code ausführt, d. h. den Teil, in dem der Code interpretiert und optimiert wird, und diskutieren wir, welche Unterschiede es zwischen den wichtigsten JavaScript-Engines gibt .

Im Allgemeinen verfügt die JavaScript-Engine über einen Verarbeitungsablauf, der einen Interpreter und einen optimierenden Compiler umfasst. Unter anderem kann der Interpreter schnell nicht optimierten Bytecode generieren, während der optimierende Compiler zwar länger braucht, aber letztendlich hochoptimierten Maschinencode generieren kann.

Dieser allgemeine Prozess ist fast derselbe wie der Workflow von V8, der in Chrome und Node.js verwendeten Javascript-Engine:

Der Interpreter in V8 heißt Ignition und ist für die Generierung und Ausführung von Bytecode verantwortlich. Während der Bytecode ausgeführt wird, werden Profilierungsdaten erfasst, die später verwendet werden können, um die Ausführung des Codes zu beschleunigen. Wenn eine Funktion heiß wird, beispielsweise wenn sie häufig ausgeführt wird, werden der generierte Bytecode und die Profilierungsdaten an Turbofan, unseren Optimierungscompiler, übergeben, um auf der Grundlage der Profilierungsdaten hochoptimierten Maschinencode zu generieren. Verstehen Sie die Grundprinzipien von JavaScript-EnginesSpiderMonkey, die von Mozilla in Firefox und Spidernode verwendete JavaScript-Engine, ist anders. Sie haben zwei optimierende Compiler statt einem. Der Interpreter durchläuft zunächst den Baseline-Compiler, um optimierten Code zu generieren. In Kombination mit den während der Ausführung des Codes gesammelten Profilierungsdaten kann der IonMonkey-Compiler dann einen höher optimierten Code generieren. Wenn der Optimierungsversuch fehlschlägt, kehrt IonMonkey zum Code in der Baseline-Phase zurück. Verstehen Sie die Grundprinzipien von JavaScript-EnginesVerstehen Sie die Grundprinzipien von JavaScript-EnginesChakra, die in Edge verwendete JavaScript-Engine von Microsoft, ist sehr ähnlich und verfügt außerdem über zwei optimierende Compiler. Der Interpreter optimiert den Code in SimpleJIT (JIT steht für Just-In-Time-Compiler, Just-in-Time-Compiler), wodurch leicht optimierter Code erzeugt wird. FullJIT kombiniert Analysedaten, um optimierteren Code zu generieren.

JavaScriptCore (abgekürzt als JSC), Apples JavaScript-Engine, die in Safari und React Native verwendet wird, bringt es mit drei verschiedenen optimierenden Compilern auf die Spitze. Der Low-Level-Interpreter LLInt optimiert den Code in den Baseline-Compiler und optimiert ihn dann in den DFG-Compiler (Data Flow Graph). Der DFG-Compiler (Data Flow Graph) kann wiederum den optimierten Code an FTL (Faster Than Light) übergeben ) zur Zusammenstellung im Schiff.

Verstehen Sie die Grundprinzipien von JavaScript-EnginesWarum verfügen einige Engines über optimierendere Compiler? Dies ist das Ergebnis einer Abwägung der Vor- und Nachteile. Interpreter können Bytecode schnell generieren, aber Bytecode ist im Allgemeinen nicht sehr effizient. Die Optimierung von Compilern dauert hingegen länger, produziert aber letztlich effizienteren Maschinencode. Es besteht ein Kompromiss zwischen einer schnellen Ausführung des Codes (Interpreter) oder einem längeren Zeitaufwand, aber letztlich der Ausführung des Codes mit optimaler Leistung (Optimierung des Compilers). Einige Engines entscheiden sich dafür, mehrere optimierende Compiler mit unterschiedlichen Zeit-/Effizienzeigenschaften hinzuzufügen, was eine feinkörnigere Kontrolle dieser Kompromisse auf Kosten zusätzlicher Komplexität ermöglicht. Ein weiterer Aspekt, der abgewogen werden muss, betrifft die Speichernutzung, auf die später in einem speziellen Artikel näher eingegangen wird.

Wir haben gerade die Hauptunterschiede beim Interpreter und der Optimierung der Compilerprozesse in jeder JavaScript-Engine hervorgehoben. Abgesehen von diesen Unterschieden haben alle JavaScript-Engines grundsätzlich die gleiche Architektur: Es gibt einen Parser und eine Art Interpreter/Compiler-Ablauf.

JavaScripts Objektmodell

Lassen Sie uns sehen, was JavaScript-Engines sonst noch gemeinsam haben, indem wir einige Aspekte der Implementierung näher betrachten.

Wie implementieren beispielsweise JavaScript-Engines das JavaScript-Objektmodell und welche Tricks verwenden sie, um den Zugriff auf Eigenschaften von JavaScript-Objekten zu beschleunigen? Es stellt sich heraus, dass alle großen Engines zu diesem Zeitpunkt ähnliche Implementierungen haben.

Die ECMAScript-Spezifikation definiert grundsätzlich alle Objekte als Wörterbücher mit Zeichenfolgenschlüsseln, die Eigenschaftseigenschaften zugeordnet sind.

Verstehen Sie die Grundprinzipien von JavaScript-EnginesZusätzlich zum [[Wert]] selbst definiert die Spezifikation auch diese Attribute:

  • [[Writable]] bestimmt, ob das Attribut neu zugewiesen werden kann,
  • [[Enumerable]] bestimmt, ob das Attribut im erscheint for in loop ,
  • [[Configurable]] bestimmt, ob das Attribut gelöscht werden kann. Die Notation für

[[eckige Klammern]] mag etwas ungewöhnlich aussehen, aber genau so definiert die Spezifikation Eigenschaften, die nicht direkt in JavaScript verfügbar gemacht werden können. In JavaScript können Sie den Eigenschaftswert eines bestimmten Objekts immer noch über die Object.getOwnPropertyDescriptor-API abrufen:

const object = { foo: 42 };Object.getOwnPropertyDescriptor(object, 'foo');// → { value: 42, writable: true, enumerable: true, configurable: true }复制代码

So definiert JavaScript Objekte, aber was ist mit Arrays?

Sie können sich ein Array als ein spezielles Objekt vorstellen. Einer der Unterschiede besteht darin, dass Arrays eine spezielle Verarbeitung für Array-Indizes durchführen. Array-Indizierung ist hier ein spezieller Begriff in der ECMAScript-Spezifikation. Arrays sind in JavaScript auf höchstens 2³²−1 Elemente beschränkt, und der Array-Index ist jeder gültige Index in diesem Bereich, d. h. jede ganze Zahl von 0 bis 2³²−2.

Ein weiterer Unterschied besteht darin, dass Arrays auch eine spezielle Längeneigenschaft haben.

const array = ['a', 'b'];
array.length; // → 2array[2] = 'c';
array.length; // → 3复制代码

In diesem Beispiel wird das Array mit der Länge 2 erstellt. Wenn wir Index 2 ein anderes Element zuweisen, wird die Länge automatisch aktualisiert.

JavaScript definiert Arrays auf ähnliche Weise wie Objekte. Beispielsweise werden alle Schlüsselwerte, einschließlich Array-Indizes, explizit als Zeichenfolgen dargestellt. Das erste Element im Array wird unter dem Schlüsselwert „0“ gespeichert. Verstehen Sie die Grundprinzipien von JavaScript-EnginesDie Eigenschaft „Länge“ ist eine weitere nicht aufzählbare und nicht konfigurierbare Eigenschaft. Wenn dem Array ein Element hinzugefügt wird, aktualisiert JavaScript automatisch die Eigenschaft [[value]] der Eigenschaft „length“. Verstehen Sie die Grundprinzipien von JavaScript-Engines

Attributzugriff optimieren

Da wir nun wissen, wie Objekte in JavaScript definiert werden, werfen wir einen genaueren Blick darauf, wie die JavaScript-Engine Objekte effizient nutzt. Insgesamt ist der Zugriff auf Eigenschaften der mit Abstand häufigste Vorgang in JavaScript-Programmen. Daher ist es wichtig, dass die JavaScript-Engine schnell auf Eigenschaften zugreifen kann.

Formen

In JavaScript-Programmen kommt es sehr häufig vor, dass mehrere Objekte dieselben Schlüsselwerteigenschaften haben. Wir können sagen, dass diese Objekte die gleiche Form haben.

const object1 = { x: 1, y: 2 };const object2 = { x: 3, y: 4 };// object1 and object2 have the same shape.复制代码

Es kommt auch sehr häufig vor, dass auf dieselben Eigenschaften von Objekten mit derselben Form zugegriffen wird:

function logX(object) {    console.log(object.x);
}const object1 = { x: 1, y: 2 };const object2 = { x: 3, y: 4 };

logX(object1);
logX(object2);复制代码

Vor diesem Hintergrund können JavaScript-Engines den Zugriff auf Objekteigenschaften basierend auf der Form des Objekts optimieren. Im Folgenden stellen wir das Prinzip vor.

Angenommen, wir haben ein Objekt mit den Attributen x und y, das die zuvor besprochene Wörterbuchdatenstruktur verwendet: Es enthält Schlüssel in Form von Zeichenfolgen, die auf ihre jeweiligen Attributwerte verweisen. Verstehen Sie die Grundprinzipien von JavaScript-Engines

Wenn Sie auf eine Eigenschaft wie object.y zugreifen, sucht die JavaScript-Engine nach dem Schlüsselwert „y“ im JSObject, lädt dann den entsprechenden Eigenschaftswert und gibt schließlich [[Wert]] zurück.

Aber wo werden diese Attributwerte im Speicher gespeichert? Sollten wir sie als Teil von JSObject speichern? Unter der Annahme, dass wir später auf weitere Objekte derselben Form stoßen, wäre es eine Verschwendung, ein vollständiges Wörterbuch mit Eigenschaftsnamen und Eigenschaftswerten im JSObject selbst zu speichern, da die Eigenschaftsnamen für alle Objekte mit dem wiederholt werden gleiche Form. Dies ist eine Menge Duplizierung und unnötiger Speicherverbrauch. Zur Optimierung speichert die Engine die Form des Objekts separat. Verstehen Sie die Grundprinzipien von JavaScript-Enginesshape enthält alle Eigenschaftsnamen und Eigenschaften außer [[Wert]]. Darüber hinaus enthält die Form den Offset des internen Werts des JSObject, sodass die JavaScript-Engine weiß, wo sie nach dem Wert suchen muss. Jedes JSObject mit derselben Form zeigt auf diese Forminstanz. Jetzt muss jedes JSObject nur noch einen Wert speichern, der für dieses Objekt eindeutig ist. Verstehen Sie die Grundprinzipien von JavaScript-EnginesDie Vorteile liegen auf der Hand, wenn wir mehrere Objekte haben. Egal wie viele Objekte es gibt, solange sie die gleiche Form haben, müssen wir die Form- und Attributinformationen nur einmal speichern!

Alle JavaScript-Engines verwenden Formen zur Optimierung, sie haben jedoch unterschiedliche Namen:

  • 学术论文称它们为 Hidden Classes(容易与 JavaScript 中的 class 混淆)
  • V8 称它们为 Maps (容易与 JavaScript 中的 Map 混淆)
  • Chakra 称它们为 Types (容易与 JavaScript 中的动态类型以及 typeof 混淆)
  • JavaScriptCore 称它们为 Structures
  • SpiderMonkey 称它们为 Shapes

本文中,我们将继续使用术语 shapes.

转换链和树

如果你有一个具有特定 shape 的对象,但你又向它添加了一个属性,此时会发生什么? JavaScript 引擎是如何找到这个新 shape 的?

const object = {};
object.x = 5;
object.y = 6;复制代码

这些 shapes 在 JavaScript 引擎中形成所谓的转换链(transition chains)。下面是一个例子:

Verstehen Sie die Grundprinzipien von JavaScript-Engines

该对象开始没有任何属性,因此它指向一个空的 shape。下一个语句为该对象添加一个值为 5 的属性 "x",所以 JavaScript 引擎转向一个包含属性 "x" 的 shape,并在第一个偏移量为 0 处向 JSObject 添加了一个值 5。 下一行添加了一个属性 'y',引擎便转向另一个包含 'x' 和 'y' 的 shape,并将值 6 添加到 JSObject(位于偏移量 1 处)。

我们甚至不需要为每个 shape 存储完整的属性表。相反,每个shape 只需要知道它引入的新属性。例如,在本例中,我们不必将有关 “x” 的信息存储在最后一个 shape 中,因为它可以在更早的链上找到。要实现这一点,每个 shape 都会链接回其上一个 shape:Verstehen Sie die Grundprinzipien von JavaScript-Engines

如果你在 JavaScript 代码中写 o.x,JavaScript 引擎会沿着转换链去查找属性 "x",直到找到引入属性 "x" 的 Shape。

但是如果没有办法创建一个转换链会怎么样呢?例如,如果有两个空对象,并且你为每个对象添加了不同的属性,该怎么办?

const object1 = {};
object1.x = 5;const object2 = {};
object2.y = 6;复制代码

在这种情况下,我们必须进行分支操作,最终我们会得到一个转换树而不是转换链。Verstehen Sie die Grundprinzipien von JavaScript-Engines

这里,我们创建了一个空对象 a,然后给它添加了一个属性 ‘x’。最终,我们得到了一个包含唯一值的 JSObject 和两个 Shape :空 shape 以及只包含属性 x 的 shape。

第二个例子也是从一个空对象 b 开始的,但是我们给它添加了一个不同的属性 ‘y’。最终,我们得到了两个 shape 链,总共 3 个 shape。

这是否意味着我们总是需要从空 shape 开始呢? 不一定。引擎对已含有属性的对象字面量会进行一些优化。比方说,我们要么从空对象字面量开始添加 x 属性,要么有一个已经包含属性 x 的对象字面量:

const object1 = {};
object1.x = 5;const object2 = { x: 6 };复制代码

在第一个例子中,我们从空 shape 开始,然后转到包含 x 的shape,这正如我们之前所见那样。

在 object2 的例子中,直接在一开始就生成含有 x 属性的对象,而不是生成一个空对象是有意义的。Verstehen Sie die Grundprinzipien von JavaScript-Engines

包含属性 ‘x’ 的对象字面量从含有 ‘x’ 的 shape 开始,有效地跳过了空 shape。V8 和 SpiderMonkey (至少)正是这么做的。这种优化缩短了转换链并且使从字面量构建对象更加高效。

下面是一个包含属性 ‘x'、'y' 和 'z' 的 3D 点对象的示例。

const point = {};
point.x = 4;
point.y = 5;
point.z = 6;复制代码

正如我们之前所了解的, 这会在内存中创建一个有3个 shape 的对象(不算空 shape 的话)。 当访问该对象的属性 ‘x’ 的时候,比如, 你在程序里写 point.x,javaScript 引擎需要循着链接列表寻找:它会从底部的 shape 开始,一层层向上寻找,直到找到顶部包含 ‘x’ 的 shape。Verstehen Sie die Grundprinzipien von JavaScript-Engines

当这样的操作更频繁时, 速度会变得非常慢,特别是当对象有很多属性的时候。寻找属性的时间复杂度为 O(n), 即和对象上的属性数量线性相关。为了加快属性的搜索速度, JavaScript 引擎增加了一种 ShapeTable 的数据结构。这个 ShapeTable 是一个字典,它将属性键映射到描述对应属性的 shape 上。

Verstehen Sie die Grundprinzipien von JavaScript-Engines

现在我们又回到字典查找了我们添加 shape 就是为了对此进行优化!那我们为什么要去纠结 shape 呢? 原因是 shape 启用了另一种称为 Inline Caches 的优化。

Inline Caches (ICs)

shapes 背后的主要动机是 Inline Caches 或 ICs 的概念。ICs 是让 JavaScript 快速运行的关键要素!JavaScript 引擎使用 ICs 来存储查找到对象属性的位置信息,以减少昂贵的查找次数。

这里有一个函数 getX,该函数接收一个对象并从中加载属性 x:

function getX(o) {    return o.x;
}复制代码

如果我们在 JSC 中运行该函数,它会产生以下字节码:Verstehen Sie die Grundprinzipien von JavaScript-Engines

第一条 get_by_id 指令从第一个参数(arg1)加载属性 ‘x’,并将结果存储到 loc0 中。第二条指令将存储的内容返回给 loc0。

JSC 还将一个 Inline Cache 嵌入到 get_by_id 指令中,该指令由两个未初始化的插槽组成。

Verstehen Sie die Grundprinzipien von JavaScript-Engines现在, 我们假设用一个对象 { x: 'a' },来执行 getX 这个函数。正如我们所知,,这个对象有一个包含属性 ‘x’ 的 shape, 该 shape存储了属性 ‘x’ 的偏移量和特性。当你在第一次执行这个函数的时候,get_by_id 指令会查找属性 ‘x’,然后发现其值存储在偏移量为 0 的位置。

Verstehen Sie die Grundprinzipien von JavaScript-Engines

嵌入到 get_by_id 指令中的 IC 存储了 shape 和该属性的偏移量:

Verstehen Sie die Grundprinzipien von JavaScript-Engines

对于后续运行,IC 只需要比较 shape,如果 shape 与之前相同,只需从存储的偏移量加载值。具体来说,如果 JavaScript 引擎看到对象的 shape 是 IC 以前记录过的,那么它根本不需要接触属性信息,相反,可以完全跳过昂贵的属性信息查找过程。这要比每次都查找属性快得多。

高效存储数组

对于数组,存储数组索引属性是很常见的。这些属性的值称为数组元素。为每个数组中的每个数组元素存储属性特性是非常浪费内存的。相反,默认情况下,数组索引属性是可写的、可枚举的和可配置的,JavaScript 引擎基于这一点将数组元素与其他命名属性分开存储。

思考下面的数组:

const array = [    '#jsconfeu',
];复制代码

引擎存储了数组长度(1),并指向包含偏移量和 'length' 属性特性的 shape。

Verstehen Sie die Grundprinzipien von JavaScript-Engines

这和我们之前看到的很相似……但是数组的值存到哪里了呢?

Verstehen Sie die Grundprinzipien von JavaScript-Engines

每个数组都有一个单独的元素备份存储区,包含所有数组索引的属性值。JavaScript 引擎不必为数组元素存储任何属性特性,因为它们通常都是可写的、可枚举的和可配置的。

那么,在非通常情况下会怎么样呢?如果更改了数组元素的属性特性,该怎么办?

// Please don’t ever do this!const array = Object.defineProperty(
    [],    '0',
    {        
        value: 'Oh noes!!1',        
        writable: false,        
        enumerable: false,        
        configurable: false,
    });复制代码

上面的代码片段定义了名为 “0” 的属性(恰好是数组索引),但将其特性设置为非默认值。

在这种边缘情况下,JavaScript 引擎将整个元素备份存储区表示成一个字典,该字典将数组索引映射到属性特性。

Verstehen Sie die Grundprinzipien von JavaScript-Engines

即使只有一个数组元素具有非默认特性,整个数组的备份存储区也会进入这种缓慢而低效的模式。避免对数组索引使用Object.defineProperty!

建议

我们已经了解了 JavaScript 引擎如何存储对象和数组,以及 shape 和 ICs 如何优化对它们的常见操作。基于这些知识,我们确定了一些可以帮助提高性能的实用的 JavaScript 编码技巧:

  • Objekte immer auf die gleiche Weise initialisieren, damit sie keine unterschiedlichen Formen haben.
  • Machen Sie sich nicht mit den Attributeigenschaften von Array-Elementen herum, damit diese effizient gespeichert und bearbeitet werden können.

Verwandte kostenlose Lernempfehlungen: Javascript (Video)

Das obige ist der detaillierte Inhalt vonVerstehen Sie die Grundprinzipien von JavaScript-Engines. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Dieser Artikel ist reproduziert unter:juejin.im. Bei Verstößen wenden Sie sich bitte an admin@php.cn löschen
Vorheriger Artikel:JavaScript-Objekte neu lernenNächster Artikel:JavaScript-Objekte neu lernen