Optimierung der Arbeit mit Prototypen in JavaScript-Engines

Das Material, dessen Übersetzung wir heute veröffentlichen, wurde von Matthias Binens und Benedict Meirer erstellt. Sie arbeiten an der V8 JS-Engine bei Google. Dieser Artikel widmet sich einigen grundlegenden Mechanismen, die nicht nur für V8, sondern auch für andere Motoren charakteristisch sind. Durch die Kenntnis der internen Struktur solcher Mechanismen können die an der JavaScript-Entwicklung Beteiligten die Probleme der Codeleistung besser steuern. Insbesondere werden wir hier über die Funktionen der Engine-Optimierungs-Pipelines sprechen und darüber, wie der Zugriff auf die Eigenschaften von Prototypen von Objekten beschleunigt werden kann.



Codeoptimierungsstufen und Kompromisse


Der Prozess der Konvertierung der Texte von in JavaScript geschriebenen Programmen in geeigneten Code zur Ausführung sieht in verschiedenen Engines ungefähr gleich aus.
Der Prozess der Konvertierung von Quell-JS-Code in ausführbaren Code

Details finden Sie hier . Darüber hinaus sollte beachtet werden, dass die Pipelines zum Konvertieren von Quellcode in ausführbare Dateien auf verschiedenen Ebenen für verschiedene Engines sehr ähnlich sind, ihre Codeoptimierungssysteme sich jedoch häufig unterscheiden. Warum ist das so? Warum haben einige Motoren mehr Optimierungsstufen als andere? Es stellt sich heraus, dass die Engines auf die eine oder andere Weise Kompromisse eingehen müssen, was darin besteht, dass sie entweder schnell Code generieren können, der nicht der effizienteste, aber für die Ausführung geeignete ist, oder mehr Zeit damit verbringen, solchen Code zu erstellen. optimale Leistung erzielen.
Schnelle Vorbereitung des Codes für die Ausführung und optimierter Code, der länger dauert, aber schneller ausgeführt wird

Der Interpreter kann schnell Bytecode generieren, aber dieser Code ist normalerweise nicht sehr effizient. Der optimierende Compiler benötigt dagegen mehr Zeit, um den Code zu generieren, erhält aber am Ende optimierten, schnelleren Maschinencode.

In V8 wird dieses Modell zur Vorbereitung des Codes für die Ausführung verwendet. Der V8-Interpreter heißt Ignition und ist der schnellste der vorhandenen Interpreter (in Bezug auf die Ausführung des Quellbytecodes). Der optimierende V8-Compiler heißt TurboFan und ist für die Erstellung hochoptimierten Maschinencodes verantwortlich.
Zündinterpreter und TurboFan-Optimierungscompiler

Der Kompromiss zwischen der Verzögerung beim Starten des Programms und der Ausführungsgeschwindigkeit ist der Grund dafür, dass einige JS-Engines zusätzliche Optimierungsstufen aufweisen. In SpiderMonkey gibt es beispielsweise zwischen dem Interpreter und dem optimierenden Compiler IonMonkey eine Zwischenebene, die vom Basis-Compiler dargestellt wird (in der Mozilla- Dokumentation wird sie als "Baseline-Compiler" bezeichnet, "Baseline" ist jedoch kein Eigenname).
SpiderMonkey-Codeoptimierungsstufen

Der Interpreter generiert schnell Bytecode, aber dieser Code wird relativ langsam ausgeführt. Der Basis-Compiler benötigt länger, um den Code zu generieren, aber dieser Code ist bereits schneller. Schließlich benötigt der optimierende IonMonkey-Compiler die meiste Zeit, um Maschinencode zu generieren, aber dieser Code kann sehr effizient ausgeführt werden.

Schauen wir uns ein bestimmtes Beispiel an und sehen wir uns an, wie Pipelines verschiedener Engines mit Code umgehen. In dem hier vorgestellten Beispiel gibt es eine „heiße“ Schleife, die Code enthält, der sich so oft wiederholt.

let result = 0; for (let i = 0; i < 4242424242; ++i) {    result += i; } console.log(result); 

V8 beginnt mit der Ausführung des Bytecodes im Ignition-Interpreter. Zu einem bestimmten Zeitpunkt stellt die Engine fest, dass der Code „heiß“ ist, und startet das TurboFan-Frontend, das Teil von TurboFan ist, das mit Profildaten arbeitet und eine grundlegende Maschinendarstellung des Codes erstellt. Die Daten werden dann zur weiteren Verbesserung an den TurboFan-Optimierer übergeben, der in einem separaten Stream arbeitet.
Hotcode-Optimierung in V8

Während der Optimierung führt V8 weiterhin Bytecode in Ignition aus. Nach Abschluss des Optimierers verfügen wir über ausführbaren Maschinencode, der in Zukunft verwendet werden kann.

Die SpiderMonkey-Engine beginnt auch, Bytecode im Interpreter auszuführen. Es gibt jedoch eine zusätzliche Ebene, die vom Basis-Compiler dargestellt wird, was dazu führt, dass der "heiße" Code zuerst zu diesem Compiler gelangt. Es generiert den Basiscode im Hauptthread, der Übergang zur Ausführung dieses Codes erfolgt, wenn er fertig ist.
Hotcode-Optimierung in SpiderMonkey

Wenn der Basiscode lange genug ausgeführt wird, startet SpiderMonkey schließlich das IonMonkey-Frontend und den Optimierer, was dem in V8 sehr ähnlich ist. Der Basiscode wird weiterhin als Teil des von IonMonkey durchgeführten Codeoptimierungsprozesses ausgeführt. Wenn die Optimierung abgeschlossen ist, wird daher der optimierte Code anstelle des Basiscodes ausgeführt.

Die Architektur der Chakra-Engine ist der Architektur von SpiderMonkey sehr ähnlich, aber Chakra strebt eine höhere Parallelität an, um ein Blockieren des Hauptthreads zu vermeiden. Anstatt Kompilierungsaufgaben im Hauptthread zu lösen, kopiert Chakra den Bytecode und die Profildaten, die der Compiler wahrscheinlich benötigt, und sendet sie an einen separaten Kompilierungsprozess.
Hot-Code-Optimierung in Chakra

Wenn der von SimpleJIT erstellte generierte Code bereit ist, führt die Engine ihn anstelle des Bytecodes aus. Dieser Vorgang wird wiederholt, um mit der Ausführung des von FullJIT erstellten Codes fortzufahren. Der Vorteil dieses Ansatzes besteht darin, dass die mit dem Kopieren von Daten verbundenen Pausen normalerweise viel kürzer sind als diejenigen, die durch den Betrieb eines vollwertigen Compilers (Front-End) verursacht werden. Das Minus dieses Ansatzes ist jedoch die Tatsache, dass heuristische Kopieralgorithmen möglicherweise einige Informationen übersehen, die für eine Art von Optimierung nützlich sein können. Hier sehen wir ein Beispiel für einen Kompromiss zwischen der Qualität des empfangenen Codes und den Verzögerungen.

In JavaScriptCore werden alle optimierenden Kompilierungsaufgaben parallel zum Hauptthread ausgeführt, der für die Ausführung des JavaScript-Codes verantwortlich ist. Es gibt jedoch keine Kopierphase. Stattdessen ruft der Hauptthread einfach Kompilierungsaufgaben in einem anderen Thread auf. Der Compiler verwendet dann ein komplexes Sperrschema, um auf Profildaten vom Hauptthread zuzugreifen.
Optimierung von "heißem" Code in JavaScriptCore

Der Vorteil dieses Ansatzes besteht darin, dass die erzwungene Blockierung des Hauptthreads durch die Ausführung von Codeoptimierungsaufgaben verringert wird. Die Nachteile dieser Architektur bestehen darin, dass ihre Implementierung die Lösung komplexer Aufgaben der Multithread-Datenverarbeitung erfordert und dass im Laufe der Arbeit zur Ausführung verschiedener Operationen auf Sperren zurückgegriffen werden muss.

Wir haben gerade die Kompromisse besprochen, die Engines eingehen müssen. Dabei haben wir zwischen der schnellen Codegenerierung mit Interpreten und der Erstellung von schnellem Code mit optimierten Compilern gewählt. Dies sind jedoch weit entfernt von allen Problemen, mit denen die Motoren konfrontiert sind. Speicher ist eine weitere Systemressource, bei der Sie auf Kompromisslösungen zurückgreifen müssen. Um dies zu demonstrieren, betrachten Sie ein einfaches JS-Programm, das Zahlen hinzufügt.

 function add(x, y) {   return x + y; } add(1, 2); 

Hier ist der Bytecode der add , die vom Ignition-Interpreter in V8 generiert wurde:

 StackCheck Ldar a1 Add a0, [0] Return 

Sie können nicht auf die Bedeutung dieses Bytecodes eingehen, da sein Inhalt für uns nicht von besonderem Interesse ist. Die Hauptsache hier ist, dass es nur vier Anweisungen hat.

Wenn ein solcher Code „heiß“ ist, wird TurboFan verwendet, der den folgenden hochoptimierten Maschinencode generiert:

 leaq rcx,[rip+0x0] movq rcx,[rcx-0x37] testb [rcx+0xf],0x1 jnz CompileLazyDeoptimizedCode push rbp movq rbp,rsp push rsi push rdi cmpq rsp,[r13+0xe88] jna StackOverflow movq rax,[rbp+0x18] test al,0x1 jnz Deoptimize movq rbx,[rbp+0x10] testb rbx,0x1 jnz Deoptimize movq rdx,rbx shrq rdx, 32 movq rcx,rax shrq rcx, 32 addl rdx,rcx jo Deoptimize shlq rdx, 32 movq rax,rdx movq rsp,rbp pop rbp ret 0x18 

Wie Sie sehen können, ist das Codevolumen im Vergleich zum obigen Beispiel von vier Anweisungen sehr groß. Typischerweise ist Bytecode viel kompakter als Maschinencode und insbesondere optimierter Maschinencode. Andererseits wird ein Interpreter benötigt, um Bytecode auszuführen, und optimierter Code kann direkt auf dem Prozessor ausgeführt werden.
Dies ist einer der Hauptgründe, warum JavaScript-Engines nicht den gesamten Code optimieren. Wie wir bereits gesehen haben, nimmt das Erstellen von optimiertem Maschinencode viel Zeit in Anspruch, und außerdem benötigt das Speichern von optimiertem Maschinencode mehr Speicher, wie wir gerade herausgefunden haben.
Speichernutzung und Optimierungsstufe

Infolgedessen können wir sagen, dass der Grund dafür, dass JS-Engines unterschiedliche Optimierungsstufen aufweisen, das grundlegende Problem der Wahl zwischen einer schnellen Codegenerierung, beispielsweise unter Verwendung eines Interpreters, und einer schnellen Codegenerierung ist, die mithilfe des optimierenden Compilers ausgeführt wird. Wenn wir über die in Engines verwendeten Ebenen der Codeoptimierung sprechen, können dem Code umso subtilere Optimierungen vorgenommen werden, je mehr davon vorhanden sind. Dies wird jedoch aufgrund der Komplexität der Engines und der zusätzlichen Belastung des Systems erreicht. Außerdem dürfen wir hier nicht vergessen, dass der Optimierungsgrad des Codes die Speichermenge beeinflusst, die dieser Code belegt. Aus diesem Grund versuchen JS-Engines, nur "heiße" Funktionen zu optimieren.

Optimierung des Zugriffs auf Eigenschaften von Objektprototypen


JavaScript-Engines optimieren den Zugriff auf Objekteigenschaften mithilfe von sogenannten Objektformularen (Shape) und Inline-Caches (Inline-Cache, IC). Details dazu finden Sie in diesem Material. Um es auf den Punkt zu bringen, können wir sagen, dass die Engine die Form des Objekts getrennt von den Werten des Objekts speichert.
Objekte mit der gleichen Form

Die Verwendung von Objektformen ermöglicht die Durchführung einer Optimierung, die als Inline-Caching bezeichnet wird. Durch die gemeinsame Verwendung von Objektformularen und Inline-Caches können Sie die wiederholten Vorgänge beim Zugriff auf die Eigenschaften von Objekten beschleunigen, die an derselben Stelle im Code ausgeführt werden.
Beschleunigen des Zugriffs auf eine Objekteigenschaft

Klassen und Prototypen


Nachdem wir nun wissen, wie Sie den Zugriff auf Objekteigenschaften in JavaScript beschleunigen können, werfen Sie einen Blick auf eine der jüngsten JavaScript-Innovationen - Klassen. So sieht die Klassendeklaration aus:

 class Bar {   constructor(x) {       this.x = x;   }   getX() {       return this.x;   } } 

Obwohl es so aussieht, als würde in JS ein völlig neues Konzept erscheinen, sind Klassen eigentlich nur syntaktischer Zucker für das Prototypsystem zum Erstellen von Objekten, das in JavaScript immer vorhanden war:

 function Bar(x) {   this.x = x; } Bar.prototype.getX = function getX() {   return this.x; }; 

Hier schreiben wir die Funktion in die getX Eigenschaft des getX Objekts. Diese Operation funktioniert genauso wie beim Erstellen der Eigenschaften eines anderen Objekts, da Prototypen in JavaScript Objekte sind. In Sprachen, die auf der Verwendung von Prototypen wie JavaScript basieren, werden Methoden, die von allen Objekten eines bestimmten Typs gemeinsam genutzt werden können, in Prototypen gespeichert, und die Felder einzelner Objekte werden in ihren Instanzen gespeichert.

Schauen wir uns an, was sozusagen hinter den Kulissen passiert, wenn wir eine neue Instanz des Bar Objekts erstellen und es dem konstanten foo zuweisen.

 const foo = new Bar(true); 

Nach dem Ausführen eines solchen Codes hat die Instanz des hier erstellten Objekts ein Formular, das eine einzelne Eigenschaft x . Der Prototyp des foo Objekts ist Bar.prototype , der zur Klasse Bar .
Objekt und sein Prototyp

Bar.prototype hat ein eigenes Formular, das eine einzelne getX Eigenschaft enthält, deren Wert eine Funktion ist, die beim Aufruf den Wert von this.x Der Prototyp Prototyp Bar.prototype ist Object.prototype , der Teil der Sprache ist. Object.prototype ist das Object.prototype des Prototypbaums, daher ist sein Prototyp null .

Nun wollen wir sehen, was passiert, wenn Sie ein anderes Objekt vom Typ Bar erstellen.
Mehrere Objekte des gleichen Typs

Wie Sie sehen können, verwenden sowohl das foo Objekt als auch das qux Objekt, die Instanzen der Bar Klasse sind, wie bereits erwähnt, dieselbe Form des Objekts. Beide verwenden denselben Prototyp - das Bar.prototype Objekt.

Zugriff auf Prototypeneigenschaften


Jetzt wissen wir also, was passiert, wenn wir eine neue Klasse deklarieren und instanziieren. Und was ist mit dem Aufruf der Methode des Objekts? Betrachten Sie das folgende Codefragment:

 class Bar {   constructor(x) { this.x = x; }   getX() { return this.x; } } const foo = new Bar(true); const x = foo.getX(); //        ^^^^^^^^^^ 

Ein Methodenaufruf kann als eine Operation verstanden werden, die aus zwei Schritten besteht:

 const x = foo.getX(); //         : const $getX = foo.getX; const x = $getX.call(foo); 

Im ersten Schritt wird die Methode geladen, die nur eine Eigenschaft des Prototyps ist (dessen Wert die Funktion ist). Im zweiten Schritt wird mit this Menge eine Funktion aufgerufen. Betrachten Sie den ersten Schritt beim Laden der getX Methode aus dem foo Objekt:
Laden der getX-Methode aus dem foo-Objekt

Die Engine analysiert das foo Objekt und stellt fest, dass es keine getX Eigenschaft in Form des foo Objekts gibt. Dies bedeutet, dass die Engine die Prototypenkette des Objekts betrachten muss, um diese Methode zu finden. Die Engine greift auf den Prototyp Bar.prototype und Bar.prototype die Objektform dieses Prototyps. Dort findet er die gewünschte Eigenschaft bei Offset 0. Als nächstes wird auf den in Bar.prototype gespeicherten Wert Bar.prototype , dort wird JSFunction getX erkannt - und genau das suchen wir. Damit ist die Suche nach der Methode abgeschlossen.

Die Flexibilität von JavaScript ermöglicht das Ändern von Prototypketten. Zum Beispiel so:

 const foo = new Bar(true); foo.getX(); // true Object.setPrototypeOf(foo, null); foo.getX(); // Uncaught TypeError: foo.getX is not a function 

In diesem Beispiel rufen wir die Methode foo.getX() zweimal auf, aber jeder dieser Aufrufe hat eine völlig andere Bedeutung und ein anderes Ergebnis. Obwohl JavaScript-Prototypen nur Objekte sind, ist es für JS-Engines noch schwieriger, den Zugriff auf Prototypeneigenschaften zu beschleunigen, als den Zugriff auf ihre eigenen Eigenschaften gewöhnlicher Objekte zu beschleunigen.

Wenn wir uns reale Programme ansehen, stellt sich heraus, dass das Laden von Prototyp-Eigenschaften eine sehr häufige Operation ist. Es wird jedes Mal ausgeführt, wenn eine Methode aufgerufen wird.

 class Bar {   constructor(x) { this.x = x; }   getX() { return this.x; } } const foo = new Bar(true); const x = foo.getX(); //        ^^^^^^^^^^ 

Zuvor haben wir darüber gesprochen, wie Engines das Laden regulärer, benutzerdefinierter Eigenschaften von Objekten mithilfe von Objektformularen und Inline-Caches optimieren. Wie kann das Laden wiederholter Prototypeneigenschaften für Objekte mit derselben Form optimiert werden? Oben haben wir gesehen, wie Eigenschaften geladen werden.
Laden der getX-Methode aus dem foo-Objekt

In unserem Fall müssen Sie Folgendes wissen, um den Zugriff auf die Methode durch wiederholte Aufrufe zu beschleunigen:

  1. Die Form des foo Objekts enthält nicht die getX Methode und ändert sich nicht. Dies bedeutet, dass das foo Objekt nicht geändert wird, indem Eigenschaften hinzugefügt oder gelöscht oder die Attribute der Eigenschaften geändert werden.
  2. Der foo Prototyp ist immer noch der ursprüngliche Bar.prototype . Dies bedeutet, dass sich der Prototyp foo nicht mit der Object.setPrototypeOf() -Methode oder durch Zuweisen eines neuen Prototyps zur speziellen _proto_ Eigenschaft _proto_ .
  3. Das Bar.prototype Formular enthält getX und ändert sich nicht. Das heißt, Bar.prototype nicht geändert, indem Eigenschaften gelöscht, hinzugefügt oder ihre Attribute geändert werden.

Im allgemeinen Fall bedeutet dies, dass wir 1 Überprüfung des Objekts selbst und 2 Überprüfungen für jeden Prototyp bis zu dem Prototyp durchführen müssen, in dem die gesuchte Eigenschaft gespeichert ist. Das heißt, Sie müssen 1 + 2N-Überprüfungen durchführen (wobei N die Anzahl der getesteten Prototypen ist), was in diesem Fall nicht so schlecht aussieht, da die Prototypenkette ziemlich kurz ist. Motoren müssen jedoch häufig mit viel längeren Prototypenketten arbeiten. Dies ist beispielsweise typisch für gewöhnliche DOM-Elemente. Hier ist ein Beispiel:

 const anchor = document.createElement('a'); // HTMLAnchorElement const title = anchor.getAttribute('title'); 

Hier haben wir HTMLAnchorElement und rufen seine getAttribute() -Methode auf. Die Prototypenkette dieses einfachen Elements, das einen HTML-Link darstellt, enthält 6 Prototypen! Die interessantesten DOM-Methoden befinden sich nicht in ihrem eigenen Prototyp HTMLAnchorElement . Sie befinden sich in Prototypen, die sich weiter unten in der Kette befinden.
Prototypkette

Die Methode getAttribute() finden Sie in Element.prototype . Dies bedeutet, dass die Engine jedes Mal, wenn die Methode anchor.getAttribute() , gezwungen ist, die folgenden Aktionen auszuführen:

  1. Überprüft das anchor selbst auf getAttribute .
  2. Überprüfen, ob der direkte Prototyp des Objekts HTMLAnchorElement.prototype .
  3. HTMLAnchorElement.prototype , dass HTMLAnchorElement.prototype keine getAttribute Methode hat.
  4. Überprüfen, ob der nächste Prototyp HTMLElement.prototype .
  5. Herauszufinden, dass es hier keine notwendige Methode gibt.
  6. Schließlich stellen Sie fest, dass der nächste Prototyp Element.prototype .
  7. Finden Sie heraus, dass es eine getAttribute Methode gibt.

Wie Sie sehen, werden hier 7 Prüfungen durchgeführt. Da solcher Code in der Webprogrammierung sehr häufig vorkommt, verwenden Engines Optimierungen, um die Anzahl der zum Laden von Prototypeneigenschaften erforderlichen Überprüfungen zu verringern.

Wenn wir zu einem der vorherigen Beispiele zurückkehren, können wir uns daran erinnern, dass wir beim Aufrufen der getX Methode des getX Objekts drei Überprüfungen durchführen:

 class Bar {   constructor(x) { this.x = x; }   getX() { return this.x; } } const foo = new Bar(true); const $getX = foo.getX; 

Für jedes Objekt in der Prototypenkette bis zu dem Objekt, das die gewünschte Eigenschaft enthält, müssen wir die Form des Objekts überprüfen, um festzustellen, ob nicht das vorhanden ist, wonach wir suchen. Es wäre schön, wenn wir die Anzahl der Überprüfungen reduzieren könnten, indem wir die Prototypprüfung auf die Überprüfung auf das Vorhandensein oder Fehlen dessen reduzieren, wonach wir suchen. Dies macht die Engine mit einem einfachen Schritt: Anstatt den Prototyp-Link in der Instanz selbst zu speichern, speichert die Engine ihn in Form eines Objekts.
Prototyp-Referenzspeicher

Jedes Formular enthält einen Link zu einem Prototyp. Dies bedeutet auch, dass sich der Motor bei foo Änderung des Prototyps foo in die neue Form des Objekts bewegt. Jetzt müssen wir nur noch die Form des Objekts auf das Vorhandensein einer Eigenschaft überprüfen und den Prototyp-Link schützen.

Dank dieses Ansatzes können wir die Anzahl der Überprüfungen von 1 + 2N auf 1 + N reduzieren, wodurch der Zugriff auf die Eigenschaften von Prototypen beschleunigt wird. Solche Operationen sind jedoch immer noch ziemlich ressourcenintensiv, da ein linearer Zusammenhang zwischen ihrer Anzahl und der Länge der Prototypenkette besteht. Die Motoren haben verschiedene Mechanismen implementiert, um sicherzustellen, dass die Anzahl der Überprüfungen nicht von der Länge der Prototypkette abhängt, ausgedrückt als Konstante. Dies gilt insbesondere in Situationen, in denen das Laden derselben Eigenschaft mehrmals ausgeführt wird.

ValidityCell-Eigenschaft


V8 bezieht sich auf die Formen von Prototypen speziell für den obigen Zweck. Jeder Prototyp hat eine eindeutige Form, die nicht mit anderen Objekten (insbesondere mit anderen Prototypen) geteilt wird, und jedem der Prototypobjektformen ist eine ValidityCell Eigenschaft zugeordnet.
ValidityCell-Eigenschaft

Diese Eigenschaft wird für ungültig erklärt, wenn der dem Formular zugeordnete Prototyp oder ein darüber liegender Prototyp geändert wird. Betrachten Sie diesen Mechanismus genauer.

Um die sequentiellen Operationen zum Laden von Eigenschaften von Prototypen zu beschleunigen, verwendet V8 einen Inline-Cache mit vier Feldern: ValidityCell , Prototype , Shape , Offset .
Inline-Cache-Felder

Während des „Aufwärmens“ des Inline-Caches beim ersten Ausführen des Codes merkt sich V8 den Versatz, bei dem die Eigenschaft im Prototyp gefunden wurde, den Prototyp, in dem die Eigenschaft gefunden wurde (in diesem Beispiel Bar.prototype ), die Form des Objekts (in diesem Fall foo ). und zusätzlich eine Verknüpfung mit dem aktuellen ValidityCell Parameter des unmittelbaren Prototyps, eine Verknüpfung in Form eines Objekts (in diesem Fall auch Bar.prototype ).

Wenn Sie das nächste Mal auf den Inline-Cache zugreifen, muss die Engine die Form des Objekts und der ValidityCell überprüfen. Wenn ValidityCell noch gültig ist, kann die Engine den zuvor im Prototyp gespeicherten Offset direkt nutzen, ohne zusätzliche Suchvorgänge auszuführen.

Wenn sich der Prototyp ändert, wird ein neues Formular erstellt und die vorherige ValidityCell Eigenschaft wird für ungültig erklärt. Wenn Sie das nächste Mal versuchen, auf den Inline-Cache zuzugreifen, hat dies keine Vorteile, was zu einer schlechten Leistung führt.
Die Folgen einer Änderung des Prototyps

Wenn wir mit dem DOM-Element zum Beispiel zurückkehren, bedeutet dies, dass jede Änderung, beispielsweise am Prototyp von Object.prototype , nicht nur dazu führt, dass der Inline-Cache für Object.prototype selbst ungültig wird, sondern auch für alle Prototypen, die sich in der Prototypenkette darunter befinden einschließlich EventTarget.prototype , Node.prototype , Element.prototype usw. bis hin zu HTMLAnchorElement.prototype .
Auswirkungen der Änderung des Object.prototype

Das Ändern des Object.prototype während der Codeausführung bedeutet in der Tat, dass die Leistung ernsthaft Object.prototype . Tu das nicht.

Wir untersuchen das Obige anhand eines Beispiels. Angenommen, wir haben die Bar Klasse und die loadX Funktion, die die Methode von Objekten loadX , die aus der Bar Klasse erstellt wurden. Wir rufen die loadX Funktion mehrmals auf und übergeben ihr Instanzen derselben Klasse.

 function loadX(bar) {   return bar.getX(); // IC  'getX'   `Bar`. } loadX(new Bar(true)); loadX(new Bar(false)); // IC  `loadX`    `ValidityCell`  // `Bar.prototype`. Object.prototype.newMethod = y => y; // `ValidityCell`  IC `loadX`   //    `Object.prototype`  . 

Der loadX Cache in loadX jetzt auf ValidityCell für Bar.prototype . , , Object.prototype — JavaScript, ValidityCell , - , .

Object.prototype — , - , . , :

 Object.prototype.foo = function() { /* … */ }; //    : someObject.foo(); //     . delete Object.prototype.foo; 

Object.prototype , - , . , . - , . , « », , .

, , . . Object.prototype , , - .

, — , JS- - , . . , , . , , , .

Zusammenfassung


, JS- , , , -, ValidityCell , . JavaScript, , ( , , , ).

Liebe Leser! , - , JS, ?

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


All Articles