Guten Tag Freunde! Der Kurs
"Sicherheit von Informationssystemen" wurde gestartet. In diesem Zusammenhang teilen wir Ihnen den letzten Teil des Artikels "Grundlagen von JavaScript-Engines: Optimierung von Prototypen" mit, dessen erster Teil
hier gelesen
werden kann .
Wir erinnern Sie auch daran, dass die aktuelle Veröffentlichung eine Fortsetzung dieser beiden Artikel ist:
„Grundlagen von JavaScript-Engines: allgemeine Formulare und Inline-Caching. Teil 1 " ,
" Grundlagen von JavaScript-Engines: Allgemeine Formulare und Inline-Caching. Teil 2 " .
Klassen und PrototypenprogrammierungNachdem wir nun wissen, wie wir schnell auf die Eigenschaften von JavaScript-Objekten zugreifen können, können wir uns die komplexere Struktur von JavaScript-Klassen ansehen. So sieht die Klassensyntax in JavaScript aus:
class Bar { constructor(x) { this.x = x; } getX() { return this.x; } }
Obwohl dies ein relativ neues Konzept für JavaScript zu sein scheint, ist es nur „syntaktischer Zucker“ für die Prototyp-Programmierung, die in JavaScript immer verwendet wurde:
function Bar(x) { this.x = x; } Bar.prototype.getX = function getX() { return this.x; };
Hier weisen wir dem Objekt
getX
Eigenschaft
getX
. Dies funktioniert genau wie bei jedem anderen Objekt, da Prototypen in JavaScript dieselben Objekte sind. In Prototyp-Programmiersprachen wie JavaScript wird über Prototypen auf Methoden zugegriffen, während Felder in bestimmten Instanzen gespeichert werden.
Schauen wir uns genauer an, was passiert, wenn wir eine neue Instanz von
Bar
erstellen, die wir
foo
nennen werden.
const foo = new Bar(true);
Eine mit diesem Code erstellte Instanz hat ein Formular mit einer einzelnen
'x'
Eigenschaft. Der
foo
Prototyp ist
Bar.prototype
, der zur
Bar
Klasse gehört.

Dieser
Bar.prototype
hat die Form von sich selbst und enthält die einzige Eigenschaft
'getX'
, deren Wert durch die Funktion
'getX'
, die beim Aufruf
this.x
Der Prototyp
Bar.prototype
ist
Object.prototype
, der Teil der JavaScript-Sprache ist.
Object.prototype
ist die Wurzel des Prototypbaums, während sein Prototyp
null
.

Wenn Sie eine neue Instanz derselben Klasse erstellen, haben beide Instanzen dieselbe Form, wie wir bereits verstanden haben. Beide Instanzen verweisen auf dasselbe
Bar.prototype
Objekt.
Zugriff auf PrototypeneigenschaftenNun wissen wir, was passiert, wenn wir eine Klasse definieren und eine neue Instanz erstellen. Aber was passiert, wenn wir die Methode für die Instanz aufrufen, wie im folgenden Beispiel?
class Bar { constructor(x) { this.x = x; } getX() { return this.x; } } const foo = new Bar(true); const x = foo.getX();
Sie können jeden Methodenaufruf als zwei separate Schritte betrachten:
const x = foo.getX();
Der erste Schritt besteht darin, die Methode zu laden, die eigentlich eine Eigenschaft des Prototyps ist (dessen Wert eine Funktion ist). Der zweite Schritt besteht darin, eine Funktion mit einer Instanz aufzurufen, beispielsweise den Wert
this
. Schauen wir uns den ersten Schritt genauer an, in dem die
getX
Methode aus der
foo
Instanz
getX
.

Die Engine startet eine Instanz von
foo
und erkennt, dass das Formular
foo
keine
'getX'
hat.
'getX'
muss es die Prototypenkette durchlaufen, um es zu finden. Wir kommen zu
Bar.prototype
, schauen uns das Prototypformular an und sehen, dass es die Eigenschaft
'getX'
bei Nullpunktverschiebung hat. Wir suchen den Wert an diesem Offset in
Bar.prototype
und finden die
JSFunction getX
, nach der wir gesucht haben.
Durch die Flexibilität von JavaScript können sich Prototyp-Kettenglieder ändern, zum Beispiel:
const foo = new Bar(true); foo.getX();
In diesem Beispiel rufen wir an
foo.getX()
zweimal, aber jedes Mal hat es völlig andere Bedeutungen und Ergebnisse. Trotz der Tatsache, dass Prototypen nur Objekte in JavaScript sind, ist die Beschleunigung des Zugriffs auf Eigenschaften eines Prototyps für JavaScript-Engines eine noch wichtigere Aufgabe als die Beschleunigung des eigenen Zugriffs auf Eigenschaften regulärer Objekte.
In der täglichen Praxis ist das Laden von Prototyp-Eigenschaften eine ziemlich häufige Operation: Dies geschieht jedes Mal, wenn Sie eine Methode aufrufen!
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 Eigenschaften mithilfe von Formularen und Inline-Caches optimieren. Wie kann ich das Laden von Prototypeneigenschaften für Objekte derselben Form optimieren? Von oben haben wir gesehen, wie Eigenschaften geladen werden.

Um dies in diesem speziellen Fall bei wiederholten Downloads schnell zu tun, müssen Sie die folgenden drei Dinge wissen:
- Das Formular
foo
enthält kein 'getX'
und wurde nicht geändert. Dies bedeutet, dass niemand das foo-Objekt geändert hat, indem er eine Eigenschaft hinzugefügt oder entfernt oder eines der Eigenschaftsattribute geändert hat. - Der foo-Prototyp ist immer noch der ursprüngliche
Bar.prototype
. Daher hat niemand den Prototyp foo
mit Object.setPrototypeOf()
geändert oder der speziellen Eigenschaft _proto_
. - Das
Bar.prototype
Formular enthält 'getX'
und wurde nicht geändert. Dies bedeutet, dass niemand Bar.prototype
durch Hinzufügen oder Entfernen einer Eigenschaft oder Ändern eines der Eigenschaftsattribute geändert hat.
Im allgemeinen Fall bedeutet dies, dass Sie eine Prüfung der Instanz selbst und zwei weitere Prüfungen für jeden Prototyp bis zu dem Prototyp durchführen müssen, der die gewünschte Eigenschaft enthält. 1 + 2N-Prüfungen, bei denen N die Anzahl der verwendeten Prototypen ist, klingen in diesem Fall nicht so schlecht, da die Prototypenkette relativ flach ist. Motoren müssen jedoch häufig mit viel längeren Prototypenketten umgehen, wie dies bei regulären DOM-Klassen der Fall ist. Zum Beispiel:
const anchor = document.createElement('a');
Wir haben ein
HTMLAnchorElement
und rufen die Methode
getAttribute()
. Die Kette für dieses einfache Element enthält bereits 6 Prototypen! Die meisten DOM-Methoden, die uns interessieren, befinden sich nicht im
HTMLAnchorElement
Prototyp
HTMLAnchorElement
, sondern irgendwo in der Kette.

Die Methode
getAttribute()
befindet sich in
Element.prototype
. Dies bedeutet, dass die JavaScript-Engine jedes Mal, wenn wir
anchor.getAttribute()
aufrufen,
anchor.getAttribute()
benötigt:
'getAttribute'
dass 'getAttribute'
per se kein 'getAttribute'
.HTMLAnchorElement.prototype
, dass der endgültige Prototyp HTMLAnchorElement.prototype
.- Bestätigen Sie dort das Fehlen von
'getAttribute'
. HTMLElement.prototype
, dass der nächste Prototyp HTMLElement.prototype
.- Bestätigen Sie das Fehlen von
'getAttribute'
. Element.prototype
, dass der nächste Prototyp Element.prototype
.- Überprüfen Sie, ob
'getAttribute'
vorhanden ist.
Insgesamt 7 Schecks. Da diese Art von Code im Web weit verbreitet ist, verwenden Engines verschiedene Tricks, um die Anzahl der zum Laden von Prototypeneigenschaften erforderlichen Überprüfungen zu verringern.
Zurück zu einem früheren Beispiel, in dem wir nur drei Überprüfungen durchgeführt haben, als wir
'getX'
für
foo
'getX'
:
class Bar { constructor(x) { this.x = x; } getX() { return this.x; } } const foo = new Bar(true); const $getX = foo.getX;
Für jedes Objekt, das vor dem Prototyp auftritt, der die gewünschte Eigenschaft enthält, müssen die Formulare auf das Fehlen dieser Eigenschaft überprüft werden. Es wäre schön, wenn wir die Anzahl der Prüfungen reduzieren könnten, indem wir die Prototypprüfung als Prüfung für das Fehlen einer Eigenschaft präsentieren. Im Wesentlichen ist dies genau das, was Engines mit einem einfachen Trick tun: Anstatt den Prototyp-Link zur Instanz selbst zu speichern, speichern die Engines ihn in Form.

Jedes Formular gibt einen Prototyp an. Dies bedeutet, dass jedes Mal, wenn sich der Prototyp
foo
ändert, der Motor in eine neue Form wechselt. Jetzt müssen wir nur noch die Form des Objekts überprüfen, um das Fehlen bestimmter Eigenschaften zu bestätigen und die Prototypverbindung zu schützen (die Prototypverbindung schützen).
Mit diesem Ansatz können wir die Anzahl der erforderlichen Überprüfungen von 2N + 1 auf 1 + N reduzieren, um den Zugriff zu beschleunigen. Dies ist immer noch eine ziemlich teure Operation, da sie immer noch eine lineare Funktion der Anzahl der Prototypen in der Kette ist. Motoren verwenden verschiedene Tricks, um die Anzahl der Überprüfungen weiter auf einen bestimmten konstanten Wert zu reduzieren, insbesondere bei sequentiellem Laden derselben Eigenschaften.
GültigkeitszellenDer V8 verarbeitet Prototypformulare speziell für diesen Zweck. Jeder Prototyp hat eine eindeutige Form, die nicht mit anderen Objekten (insbesondere mit anderen Prototypen) geteilt wird, und jeder dieser Prototypformen ist eine spezielle
ValidityCell
zugeordnet.

Diese
ValidityCell
jedes Mal deaktiviert, wenn jemand den damit verbundenen Prototyp oder einen anderen darüber liegenden Prototyp ändert. Mal sehen, wie es funktioniert.
Um nachfolgende Prototyp-Downloads zu beschleunigen, platziert V8 den Inline-Cache an einem Ort mit vier Feldern:

Wenn der Inline-Cache beim ersten Ausführen des Codes erwärmt wird, merkt sich V8 den Offset, bei dem die Eigenschaft im Prototyp gefunden wurde, diesen Prototyp (z. B.
Bar.prototype
), das
Bar.prototype
(in unserem Fall das Formular
foo
) und bindet auch die aktuelle
ValidityCell
an den empfangenen Prototyp aus der Instanz des Formulars (in unserem Fall wird
Bar.prototype
übernommen).
Wenn Sie das nächste Mal den Inline-Cache verwenden, muss die Engine das Instanzformular und
ValidityCell
überprüfen. Wenn es noch gültig ist, verwendet die Engine direkt den Offset des Prototyps und überspringt die zusätzlichen Suchschritte.

Wenn Sie den Prototyp ändern, wird ein neues Formular hervorgehoben und die vorherige
ValidityCell
Zelle deaktiviert. Aus diesem Grund wird der Inline-Cache beim nächsten Start übersprungen, was zu einer schlechten Leistung führt.
Kehren wir zum Beispiel mit dem DOM-Element zurück. Jede Änderung in
Object.prototype
nicht nur die Inline-Caches für
Object.prototype
ungültig, sondern auch für alle Prototypen in der darunter
EventTarget.prototype
Kette, einschließlich
EventTarget.prototype
,
Node.prototype
,
Element.prototype
usw. bis hin zu
HTMLAnchorElement.prototype
.

Tatsächlich ist das Ändern von
Object.prototype
während der Ausführung des Codes ein schrecklicher Leistungsverlust. Tu das nicht!
Schauen wir uns ein konkretes Beispiel an, um besser zu verstehen, wie dies funktioniert.
loadX
, wir haben eine
Bar
Klasse und eine
loadX
Funktion, die eine Methode für Objekte vom Typ
Bar
aufruft. Wir rufen die
loadX
Funktion mehrmals mit Instanzen derselben Klasse auf.
class Bar { } function loadX(bar) { return bar.getX();
Der Inline-Cache in
loadX
jetzt auf
ValidityCell
für
Bar.prototype
. Wenn Sie dann den (mutierten)
Object.prototype
, der die Wurzel aller Prototypen in JavaScript darstellt, wird
ValidityCell
ungültig und vorhandene Inline-Caches werden beim nächsten Mal nicht verwendet, was zu einer schlechten Leistung führt.
Das Ändern des
Object.prototype
ist immer eine schlechte Idee, da zum Zeitpunkt der Änderung alle Inline-Caches für geladene Prototypen ungültig werden. Hier ist ein Beispiel, wie man NICHT macht:
Object.prototype.foo = function() { };
Wir erweitern
Object.prototype
, wodurch alle von der Engine zu diesem Zeitpunkt geladenen Inline-Prototyp-Caches ungültig werden. Dann werden wir einen Code ausführen, der die von uns beschriebene Methode verwendet. Die Engine muss von Anfang an starten und Inline-Caches für den Zugriff auf die Prototyp-Eigenschaft konfigurieren. Und schließlich „bereinigen“ und entfernen Sie die zuvor hinzugefügte Prototypmethode.
Sie denken, Reinigung ist eine gute Idee, oder? In diesem Fall wird sich die Situation weiter verschlechtern! Durch das Entfernen von Eigenschaften wird
Object.prototype
, sodass alle Inline-Caches wieder deaktiviert werden und die Engine die Arbeit von
Object.prototype
beginnen muss.
Zusammenfassend . Obwohl Prototypen nur Objekte sind, werden sie speziell von JavaScript-Engines verarbeitet, um die Leistung der Methodensuche nach Prototypen zu optimieren.
Lass die Prototypen in Ruhe! Oder wenn Sie sich wirklich mit ihnen befassen müssen, tun Sie dies, bevor Sie den Code ausführen, damit Sie zumindest nicht alle Versuche ungültig machen, Ihren Code während seiner Ausführung zu optimieren!
Fassen Sie zusammen
Wir haben gelernt, wie JavaScript Objekte und Klassen speichert und wie Formulare, Inline-Caches und Gültigkeitszellen zur Optimierung von Prototypoperationen beitragen. Basierend auf diesem Wissen haben wir verstanden, wie die Leistung aus praktischer Sicht verbessert werden kann: Berühren Sie keine Prototypen! (oder wenn Sie es wirklich brauchen, tun Sie es, bevor Sie den Code ausführen).
←
Der erste TeilWar diese Reihe von Veröffentlichungen für Sie hilfreich? Schreiben Sie in die Kommentare.