JIT in PHP 8 verstehen

Die Übersetzung des Artikels wurde am Vorabend des Kursbeginns "Backend-Entwickler in PHP" vorbereitet.








TL; DR



Der Just In Time-Compiler in PHP 8 ist als Teil der Opcache-Erweiterung implementiert und dient zur Laufzeit zum Kompilieren von Betriebscode in Prozessoranweisungen.



Dies bedeutet, dass mit JIT einige Betriebscodes nicht von Zend VM interpretiert werden müssen. Solche Anweisungen werden direkt als Anweisungen auf Prozessorebene ausgeführt.



JIT in PHP 8



Eine der am meisten kommentierten Funktionen von PHP 8 ist der JIT-Compiler (Just In Time). Es ist in vielen Blogs und Communities zu hören - es gibt viel Aufsehen, aber bisher habe ich nicht viele Details darüber gefunden, wie JIT im Detail funktioniert.



Nach vielen Versuchen und Frustrationen, nützliche Informationen zu finden, entschied ich mich, den PHP-Quellcode zu studieren. Durch die Kombination meiner geringen C-Kenntnisse mit all den verstreuten Informationen, die ich bisher sammeln konnte, habe ich es geschafft, diesen Artikel vorzubereiten, und hoffe, dass er Ihnen hilft, PHP JIT besser zu verstehen.



Um die Dinge einfach zu halten: Wenn JIT wie erwartet funktioniert, wird Ihr Code nicht über die Zend-VM ausgeführt, sondern direkt als eine Reihe von Anweisungen auf Prozessorebene.



Das ist die ganze Idee.



Aber um dies besser zu verstehen, müssen wir darüber nachdenken, wie PHP intern funktioniert. Es ist nicht sehr schwierig, aber es bedarf einer Einführung.



Ich habe bereits einen Artikel mit einem schnellen Überblick über die Funktionsweise von PHP geschrieben . Wenn Sie der Meinung sind, dass dieser Artikel zu kompliziert wird, lesen Sie einfach den Vorgänger und kehren Sie zurück. Dies sollte die Dinge etwas einfacher machen.



Wie wird PHP-Code ausgeführt?



Wir alle wissen, dass PHP eine interpretierte Sprache ist. Aber was bedeutet das eigentlich?



Wann immer Sie PHP-Code ausführen möchten, sei es ein Snippet oder eine ganze Webanwendung, müssen Sie den PHP-Interpreter durchgehen. Die am häufigsten verwendeten sind PHP FPM und der CLI-Interpreter. Ihre Aufgabe ist sehr einfach: Holen Sie sich den PHP-Code, interpretieren Sie ihn und geben Sie das Ergebnis zurück.



Dies ist ein gemeinsames Bild für jede interpretierte Sprache. Einige Schritte können variieren, aber die allgemeine Idee ist dieselbe. In PHP funktioniert es so:



  1. PHP-Code wird gelesen und in eine Reihe von Schlüsselwörtern umgewandelt, die als Tokens bezeichnet werden. Dieser Prozess ermöglicht es dem Interpreter zu verstehen, in welchen Teil des Programms jeder Code geschrieben ist. Dieser erste Schritt wird als Lexing oder Tokenizing bezeichnet .
  2. , PHP . (Abstract Syntax Tree — AST) , (parsing). AST , , . , «echo 1 + 1» « 1 + 1» , , « , — 1 + 1».
  3. AST, , . -, , (Intermediate Representation IR), PHP (Opcode). AST .
  4. Jetzt, wo wir die Opcodes haben, kommt das Interessanteste: die Implementierung des Codes! PHP verfügt über eine Engine namens Zend VM, die in der Lage ist, eine Liste von Opcodes abzurufen und auszuführen. Nachdem alle Opcodes ausgeführt wurden, endet das Programm.




Um es etwas klarer zu machen, habe ich ein Diagramm erstellt:





Ein vereinfachtes Diagramm des PHP-Interpretationsprozesses.



Ziemlich einfach, wie Sie sehen können. Aber hier gibt es auch einen Engpass: Was bringt es, Ihren Code jedes Mal zu lexen und zu analysieren, wenn Sie ihn ausführen, wenn sich Ihr PHP-Code möglicherweise nicht einmal so oft ändert?



Wir interessieren uns doch nur für Opcodes, oder? Richtig! Aus diesem Grund ist die Opcache-Erweiterung vorhanden .



Opcache-Erweiterung



Die Opcache-Erweiterung wird mit PHP geliefert und es gibt normalerweise keinen besonderen Grund, sie zu deaktivieren. Wenn Sie PHP verwenden, sollten Sie wahrscheinlich Opcache aktivieren.



Es wird eine gemeinsam genutzte Online-Opcode-Cache-Schicht hinzugefügt. Seine Aufgabe ist es, die kürzlich generierten Opcodes von unserem AST abzurufen und zwischenzuspeichern, damit spätere Ausführungen die Lexing- und Parsing-Phasen leicht überspringen können.



Hier ist ein Diagramm des gleichen Prozesses mit Blick auf die Opcache-Erweiterung:





PHP-Interpretationsfluss mit Opcache. Wenn die Datei bereits analysiert wurde, extrahiert PHP den zwischengespeicherten Opcode dafür, anstatt ihn erneut zu analysieren.



Es ist einfach faszinierend, wie schön die Schritte zum Lexen, Parsen und Kompilieren übersprungen werden.

Hinweis : Hier bietet sich die PHP 7.4 Preload-Funktion an ! Auf diese Weise können Sie PHP FPM anweisen, Ihre Codebasis zu analysieren, sie in Opcodes zu konvertieren und sie zwischenzuspeichern, bevor Sie tatsächlich etwas tun.


Sie werden sich vielleicht fragen, wo Sie JIT hier anbringen können, oder ?! Zumindest hoffe ich das, weshalb ich diesen Artikel schreibe ...



Was macht der Just In Time-Compiler?



Nachdem ich mir Zivs Erklärung in einer Folge von PHP- und JIT-Podcasts von PHP Internals News angehört hatte , konnte ich mir ein



Bild davon machen, was die JIT eigentlich tun soll ... Wenn Opcache das schnellere Abrufen von Opcode ermöglicht, damit es direkt zur Zend VM, JIT, springen kann soll es ohne Zend VM überhaupt funktionieren lassen.



Zend VM ist ein C-Programm, das als Schicht zwischen dem Betriebscode und dem Prozessor selbst fungiert. Die JIT generiert den kompilierten Code zur Laufzeit, sodass PHP die Zend-VM überspringen und direkt zum Prozessor springen kann . Theoretisch sollten wir davon in Bezug auf die Leistung profitieren.



Anfangs klang es seltsam, denn um Maschinencode zu kompilieren, muss man für jeden Architekturtyp eine ganz bestimmte Implementierung schreiben. Aber in der Tat ist es ziemlich real.



Die JIT-Implementierung in PHP verwendet die DynASM- Bibliothek (Dynamic Assembler) , die eine Reihe von CPU-Anweisungen in einem bestimmten Format dem Assembler-Code für viele verschiedene CPU-Typen zuordnet. Daher konvertiert der Just In Time-Compiler mit DynASM Betriebscode in architekturspezifischen Maschinencode.



Obwohl mich immer noch ein Gedanke verfolgte ...



Wenn das Preloading in der Lage ist, PHP-Code vor der Ausführung auf betriebsbereit zu analysieren, und DynASM betriebsbereiten Code auf Maschinencode kompilieren kann (Just In Time-Kompilierung), warum zum Teufel kompilieren wir PHP nicht sofort mithilfe der Ahead of Time-Kompilierung?!



Einer der Gedanken, die ich aus der Podcast-Episode bekam, war, dass PHP schwach typisiert ist, was bedeutet, dass PHP oft nicht weiß, welcher Typ eine Variable ist, bis Zend VM versucht, einen bestimmten Opcode auszuführen.



Sie können dies verstehen, indem Sie sich den Vereinigungstyp zend_value ansehen , der viele Zeiger auf verschiedene Typdarstellungen für eine Variable enthält. Immer wenn Zend VM versucht, einen Wert aus zend_value abzurufen, werden Makros wie ZSTR_VAL verwendetdie versuchen, über die Wertverkettung auf den Zeichenfolgenzeiger zuzugreifen.



Beispielsweise muss dieser Zend VM-Handler den Ausdruck kleiner oder gleich (<=) verarbeiten. Sehen Sie, wie es in viele verschiedene Codepfade verzweigt, um die Typen der Operanden zu erraten.



Das Duplizieren dieser Inferenzlogik mit Maschinencode ist nicht möglich und kann die Dinge möglicherweise noch langsamer machen.



Die endgültige Kompilierung nach Auswertung der Typen ist ebenfalls keine gute Option, da das Kompilieren in Maschinencode eine CPU-intensive Aufgabe ist. Daher ist es eine schlechte Idee, ALLES zur Laufzeit zu kompilieren.



Wie verhält sich der Just In Time-Compiler?



Wir wissen jetzt, dass wir keine Typen ableiten können, um eine ausreichend gute Vorkompilierung zu generieren. Wir wissen auch, dass das Kompilieren zur Laufzeit teuer ist. Wie kann JIT für PHP nützlich sein?



Um diese Gleichung auszugleichen, versucht PHP JIT, nur einige Opcodes zu kompilieren, die es für wert hält. Zu diesem Zweck werden die von der virtuellen Maschine von Zend ausgeführten Opcodes profiliert und geprüft, welche zum Kompilieren sinnvoll sind. (abhängig von Ihrer Konfiguration) .



Wenn ein bestimmter Opcode kompiliert wird, delegiert er die Ausführung an diesen kompilierten Code, anstatt an die Zend-VM zu delegieren. Es sieht aus wie im folgenden Diagramm:





PHP-Interpretationsfluss mit JIT. Wenn sie bereits kompiliert sind, werden die Opcodes nicht über die Zend-VM ausgeführt.



Daher enthält die Opcache-Erweiterung einige Anweisungen, die bestimmen, ob ein bestimmter Betriebscode kompiliert werden soll oder nicht. In diesem Fall konvertiert der Compiler ihn mit DynASM in Maschinencode und führt diesen neu generierten Maschinencode aus.



Interessanterweise sollte die Codeausführung nahtlos zwischen JIT und interpretiertem Code wechseln können, da die aktuelle Implementierung ein Megabyte-Limit für kompilierten Code hat (auch konfigurierbar).



Übrigens hat mir dieser Vortrag von Benoit Jacquemont über JIT von PHP SEHR geholfen, dies herauszufinden.



Ich bin mir immer noch nicht sicher, in welchen konkreten Fällen die Zusammenstellung stattfindet, aber ich glaube, ich möchte das noch nicht wirklich wissen.



Ihr Produktivitätsgewinn wird also wahrscheinlich nicht kolossal sein



Ich hoffe, es ist jetzt viel klarer, WARUM alle sagen, dass die meisten PHP-Anwendungen durch die Verwendung des Just In Time-Compilers keinen großen Leistungsvorteil erzielen. Und warum die Empfehlung von Ziv zum Profilieren und Experimentieren mit verschiedenen JIT-Konfigurationen für Ihre Anwendung der beste Weg ist.



Kompilierte Opcodes werden normalerweise auf mehrere Anforderungen verteilt, wenn Sie PHP FPM verwenden. Dies ist jedoch immer noch kein Game Changer.



Dies liegt daran, dass JIT den CPU-Betrieb optimiert und heutzutage die meisten PHP-Anwendungen mehr E / A-gebunden sind als alles andere. Es spielt keine Rolle, ob die Verarbeitungsvorgänge kompiliert werden, wenn Sie trotzdem auf die Festplatte oder das Netzwerk zugreifen müssen. Die Timings werden sehr ähnlich sein.



Wenn nur...



Sie machen etwas Nicht-E / A, wie Bildverarbeitung oder maschinelles Lernen. Alles andere als E / A profitiert vom Just In Time-Compiler. Dies ist auch der Grund, warum die Leute jetzt sagen, dass sie eher dazu neigen, native PHP-Funktionen zu schreiben, die in PHP geschrieben sind, als in C. Der Overhead wird sich nicht dramatisch unterscheiden, wenn solche Funktionen sowieso kompiliert werden.



Eine interessante Zeit als PHP-Programmierer ...



Ich hoffe, dieser Artikel war hilfreich für Sie und Sie haben ein besseres Verständnis dafür, was JIT in PHP 8 ist. Sie können mich gerne twittern, wenn Sie etwas hinzufügen möchten, das ich möglicherweise vergessen habe und vergessen Sie nicht, dies mit Ihren Mitentwicklern zu teilen, es wird sicherlich ein wenig Wert für Ihre Gespräche schaffen!-- @nawarian






PHP:







All Articles