Blog 4 Min. Lesezeit

Performance mit der V8 Engine steigern: Eine Studie zu Designentscheidungen

Diesen Artikel teilen
Boost Performance with V8 Engine: A Design Choices Study

Die V8 Engine ist Googles quelloffene, leistungsstarke JavaScript-Engine, die in C++ geschrieben ist. Neben Google Chrome kommt sie auch in MongoDB, Node.js und vielen weiteren verbreiteten Anwendungen zum Einsatz.

Es ist interessant zu untersuchen, was V8 so schnell macht und mit welchen Techniken dieses Leistungsniveau erreicht wird.

OOP, Entwurfsmuster und Performance

Manche C/C++-Entwickler vertreten irrtümlich die Ansicht, dass OOP und Entwurfsmuster die Performance einer Anwendung zwangsläufig verschlechtern. V8 zeigt sehr gut, dass dies nicht unbedingt der Fall ist. Die Engine setzt zahlreiche Entwurfsmuster ein und ist dennoch hervorragend optimiert.

Zu den verwendeten Mustern gehören unter anderem:

Factory

Wenn die JavaScript-Engine ein Skript ausführt, erzeugt sie für jede Variable, Funktion und jedes Array, auf das sie trifft, eine Instanz. JSObject ist die Basisklasse all dieser Objektarten.

Hier ist eine Liste aller Klassen, die von JSObject erben:

v1

V8 implementiert eine Factory-Klasse zur Erzeugung der benötigten Objekte. Dafür wird die Methode Factory::NewJsObject verwendet.

Hier sind alle Methoden, die sie verwenden:

v4

Die Factory wird von den V8-Klassen nicht direkt verwendet, sondern über die Klasse Heap aufgerufen. Dadurch entsteht in der Implementierung eine zusätzliche Indirektionsebene.

Visitor

Wie auf der Wiki-Seite zum Visitor-Pattern erläutert:

The visitor design pattern is a way of separating an algorithm from an object structure on which it operates. A practical result of this separation is the ability to add new operations to existing object structures without modifying those structures. It is one way to follow the open/closed principle.

Wie das Factory-Pattern fügt auch dieses Muster der Implementierung eine gewisse Indirektion hinzu, macht den Code dafür jedoch besser lesbar und wartbarer.

Der V8-Quellcode enthält zahlreiche Klassen, die das Visitor-Pattern implementieren.

v2

Obwohl die V8-Entwickler die Ausführungsleistung optimieren müssen, nehmen sie bewusst gewisse Indirektionen im Code in Kauf. Entwurfsmuster und einige C++-Mechanismen können durch die zusätzliche Indirektion ihrer Implementierung zwar die Performance beeinflussen, doch dabei geht es eher um Mikrooptimierungen. Wesentliche Optimierungen auf Makroebene hängen stärker von Designentscheidungen ab, die auf die Anforderungen der jeweiligen Anwendung zugeschnitten sind.

Designentscheidungen in V8 zur Optimierung der Ausführung

1 – Hidden Classes und schneller Eigenschaftszugriff

JavaScript ist eine dynamische Programmiersprache: Eigenschaften können Objekten zur Laufzeit hinzugefügt oder aus ihnen entfernt werden. Die Eigenschaften eines Objekts können sich daher im Laufe der Zeit ändern.

Wie bereits erwähnt, ist JSObject die Basisklasse von JSFunction, das eine JavaScript-Funktion repräsentiert, sowie von JSValue, das einen JavaScript-Wert repräsentiert. Es gibt jedoch keine von JSObject abgeleitete Klasse, die eine JavaScript-Klasse wie Function oder Value abbildet. Die meisten JavaScript-Engines speichern Objekteigenschaften in einer wörterbuchähnlichen Datenstruktur; bei jedem Zugriff auf eine Eigenschaft ist daher eine dynamische Suche erforderlich, um deren Speicherposition zu bestimmen.

Dieser Ansatz macht den Zugriff auf Eigenschaften in JavaScript in der Regel deutlich langsamer als den Zugriff auf Instanzvariablen in Programmiersprachen wie Java oder Smalltalk. Dort liegen Instanzvariablen aufgrund des durch die Klasse festgelegten Objektlayouts an festen, vom Compiler bestimmten Offsets. Der Zugriff besteht somit lediglich aus einem Speicher-Lese- oder Schreibvorgang und benötigt häufig nur eine einzige Instruktion.

V8 nutzt das Konzept der Hidden Classes, um die für den Zugriff auf JavaScript-Eigenschaften benötigte Zeit zu reduzieren. Statt einer dynamischen Suche erzeugt V8 im Hintergrund zur Laufzeit sogenannte Hidden Classes.

2 – Dynamische Erzeugung von Maschinencode

V8 kompiliert JavaScript-Quellcode bei seiner ersten Ausführung direkt in Maschinencode. Es gibt weder einen zwischengeschalteten Bytecode noch einen Interpreter. Eigenschaftszugriffe werden durch Inline-Cache-Code verarbeitet, der während der Ausführung von V8 mit anderen Maschineninstruktionen angepasst werden kann.

3 – Effiziente Garbage Collection

V8 gibt den Speicher nicht mehr benötigter Objekte in einem als Garbage Collection bezeichneten Prozess wieder frei. Um eine schnelle Objektallokation, kurze Garbage-Collection-Pausen und möglichst keine Speicherfragmentierung zu gewährleisten, verwendet V8 einen präzisen, generationellen Stop-the-World-Garbage-Collector. Das bedeutet, dass V8:

  • die Programmausführung während eines Garbage-Collection-Zyklus anhält.
  • in den meisten Garbage-Collection-Zyklen nur einen Teil des Objekt-Heaps verarbeitet und dadurch die Auswirkungen des Anhaltens der Anwendung minimiert.
  • jederzeit genau weiß, wo sich sämtliche Objekte und Zeiger im Speicher befinden. Dadurch wird vermieden, Objekte fälschlicherweise als Zeiger zu interpretieren, was zu Speicherlecks führen könnte.

Fazit

Auf OOP und Entwurfsmuster allein deshalb zu verzichten, weil Performance oberste Priorität hat, kann eine schlechte Entscheidung sein: Vielleicht gewinnen Sie einige Mikrosekunden, verlieren dafür aber an Lesbarkeit und Wartbarkeit Ihres Codes.

Wesentliche Optimierungen auf Makroebene hängen stärker von Designentscheidungen ab, die auf die Anforderungen Ihrer Anwendung zugeschnitten sind.

Diesen Artikel teilen