Das Interessanteste in PHP 8

PHP 7.4 wurde gerade für stabil erklärt und wir haben bereits weitere Verbesserungen vorgelegt. Und das Beste daran, worauf PHP wartet, ist Dmitry Stogov - einer der führenden Entwickler von Open Source PHP und wahrscheinlich der älteste aktive Mitwirkende.

In allen Berichten von Dmitry geht es nur um die Technologien und Lösungen, an denen er persönlich arbeitet. In den besten Traditionen von Ontiko, unter dem Schnitt, eine Textversion der Geschichte über die interessantesten aus der Sicht von Dmitry Innovationen von PHP 8, die neue Anwendungsfälle eröffnen können. Zuallererst JIT und FFI - nicht in den Schlüssel der „erstaunlichen Aussichten“, sondern mit Implementierungsdetails und Fallstricken.


Als Referenz: Dmitry Stogov lernte die Programmierung 1984 kennen, als nicht alle Leser geboren wurden, und konnte einen bedeutenden Beitrag zur Entwicklung von Entwicklungswerkzeugen und insbesondere PHP leisten (obwohl Dmitry die PHP-Leistung nicht speziell für russische Entwickler verbessert, äußerten sie sich Mein Dank geht an den HighLoad ++ Award. Dmitry ist der Autor von Turck MMCache für PHP (eAccelerator), Zend OPcache-Betreuer, Leiter des PHPNG-Projekts, das die Basis von PHP 7 bildete, und Leiter der Entwicklung von JIT für PHP.

PHP-Performance-Entwicklung


Ich habe vor 15 Jahren angefangen, an PHP-Performance zu arbeiten, als ich zu Zend kam. Dann haben wir Version 5.0 veröffentlicht - die erste, in der die Sprache wirklich objektorientiert wurde. Seitdem konnten wir die Leistung bei synthetischen Tests um das 40-fache und bei realen Anwendungen um das 6-fache verbessern.



In dieser Zeit gab es zwei Durchbruchsmomente:

  • Version 5.1, mit der wir die Interpretationsgeschwindigkeit deutlich steigern konnten. Wir haben einen spezialisierten Dolmetscher implementiert, der sich hauptsächlich auf die synthetischen Tests auswirkte.
  • Version 7.0, in der alle wichtigen Datenstrukturen verarbeitet und damit die Arbeit mit Speicher und Prozessor-Cache optimiert wurden (mehr zu diesen Optimierungen hier ). Dies führte sowohl in synthetischen Tests als auch in realen Anwendungen zu einer mehr als zweifachen Beschleunigung.

Bei allen anderen Versionen wurde die Produktivität schrittweise gesteigert, indem viele weniger effektive Ideen umgesetzt wurden. In Version 7.1 wurde beispielsweise viel Wert auf die Optimierung des Bytecodes gelegt ( ein Artikel zu diesen Lösungen).

Das Diagramm zeigt, dass wir sowohl am Ende der Entwicklung der 5. Version als auch am Ende des Entwicklungszyklus der 7. Version auf ein Plateau gehen und langsamer werden. Im letzten Jahr der Arbeit an v7.4 wurde daher nur eine Produktivitätssteigerung von 2% erzielt. Und das ist nicht schlecht, weil neue Funktionen wie typisierte Eigenschaften und kovariante Typen aufgetaucht sind, die PHP verlangsamen (Nikita Popov sprach über diese neuen Produkte in PHP Russland).

Und jetzt wundert sich jeder, was von der 8. Version zu erwarten ist. Kann sie den Erfolg von v7 wiederholen?

JIT oder nicht JIT


Die Ideen zur Verbesserung des Dolmetschers sind noch nicht ausgeschöpft, aber alle erfordern eine sehr gründliche Untersuchung. Viele von ihnen müssen im Stadium des Proof-of-Concept abgelehnt werden, da sich herausstellt, dass der erzielbare Gewinn mit der Komplikation oder den auferlegten technischen Einschränkungen nicht vereinbar ist.

Es bleibt jedoch Hoffnung auf eine neue bahnbrechende Technologie - natürlich erinnere ich mich an die JIT und die Erfolgsgeschichte von JavaScript-Engines.

Tatsächlich wird seit 2012 an JIT für PHP gearbeitet. Es gab drei oder vier Implementierungen, wir arbeiteten mit Intel-Kollegen und JavaScript-Hackern zusammen, aber irgendwie war es nicht möglich, JIT in den Hauptzweig aufzunehmen. Letztendlich haben wir in PHP 8 JIT in den Compiler aufgenommen und eine doppelte Beschleunigung festgestellt, jedoch nur bei synthetischen Tests, im Gegenteil bei realen Anwendungen.



Das ist natürlich nicht das, wonach wir streben.

Was ist los Vielleicht machen wir etwas falsch, vielleicht ist WordPress so schlecht und kein JIT wird ihm helfen (ja, eigentlich ist es das). Vielleicht haben wir den Interpreter schon zu gut gemacht, aber in JavaScript ist es schlimmer. In Rechentests ist dies wahr: Der PHP-Interpreter ist einer der besten .



Beim Mandelbrot-Test überholt er sogar Juwelen wie LuaJIT, einen in Assemblersprache geschriebenen Dolmetscher. In diesem Test sind wir nur viermal hinter dem optimierenden GCC-5.3-Compiler zurück. Mit JIT konnten wir im Mandelbrot-Test bessere Ergebnisse erzielen. Tatsächlich tun wir dies bereits, das heißt, wir können Code generieren, der mit dem C-Compiler konkurriert.

Warum können wir dann reale Anwendungen nicht beschleunigen? Zum besseren Verständnis erkläre ich Ihnen, wie wir JIT durchführen. Beginnen wir mit den Grundlagen.

So funktioniert PHP



Der Server akzeptiert die Anforderung, kompiliert sie in Bytecode, der wiederum zur Ausführung an die virtuelle Maschine gesendet wird. Durch die Ausführung des Bytecodes kann die virtuelle Maschine auch andere PHP-Dateien aufrufen, die erneut in Bytecode übersetzt und erneut ausgeführt werden.

Nach Abschluss der Abfrage werden alle diesbezüglichen Informationen, einschließlich des Bytecodes, aus dem Speicher gelöscht. Das heißt, jedes PHP-Skript muss bei jeder Anforderung erneut kompiliert werden. Natürlich ist es einfach unmöglich, die JIT-Kompilierung in ein solches Schema einzubetten, da der Compiler sehr schnell sein muss.

Aber höchstwahrscheinlich benutzt niemand PHP in seiner bloßen Form, jeder benutzt es mit OPcache.

PHP + OPcache



Das Hauptziel von OPcache ist es, das Neukompilieren von Skripten bei jeder Anforderung zu vermeiden. Es ist in einen speziell dafür entwickelten Punkt eingebettet, fängt alle Kompilierungsanforderungen ab und speichert den kompilierten Bytecode im gemeinsamen Speicher.

Gleichzeitig wird nicht nur die Kompilierungszeit, sondern auch der Speicherplatz gespart, da zuvor im Adressraum jedes Prozesses Bytecode-Speicher zugewiesen wurde und dieser jetzt in einer einzigen Kopie vorhanden ist.

Sie können JIT bereits in diese Schaltung einbetten, was wir tun werden. Aber zuerst zeige ich Ihnen, wie der Dolmetscher funktioniert.



Ein Interpreter ist zuallererst eine Schleife, die für jede Anweisung einen eigenen Handler aufruft.

Wir verwenden zwei Register:

  • execute_data - Zeiger auf den aktuellen Aktivierungsrahmen;
  • opline - Zeiger auf die aktuelle ausführbare virtuelle Anweisung.

Mit der Erweiterung gcc werden diese beiden Registertypen auf reale Hardwareregister abgebildet, und aufgrund dessen arbeiten sie sehr schnell.

In der Schleife rufen wir einfach den Handler für jede Anweisung auf, wonach wir am Ende jeder Prozedur den Zeiger auf die nächste Anweisung bewegen.

Es ist wichtig zu beachten, dass die Adresse des Handlers direkt in den Bytecode geschrieben wird. Es kann mehrere verschiedene Handler für eine einzelne Anweisung geben. Dies wurde ursprünglich zur Spezialisierung erfunden, damit sich Handler auf Operandentypen spezialisieren können. Dieselbe Technologie wird für JIT verwendet, denn wenn Sie die Adresse als Handler in den neu generierten Code schreiben, werden JIT-Handler ohne Änderungen im Interpreter gestartet.

Im obigen Beispiel ist der für die Additionsanweisung geschriebene Handler rechts dargestellt. Es werden Operanden benötigt (hier können die erste und die zweite Variable eine Konstante, eine temporäre oder eine lokale Variable sein), Operanden gelesen, Typen überprüft, eine direkte Logik erzeugt - Addition - und dann zur Schleife zurückgekehrt, die die Steuerung an den nächsten Handler übergibt.

Aus dieser Beschreibung werden spezielle Funktionen generiert. Da es drei mögliche erste Operanden gab, drei mögliche zweite, erhalten wir 9 verschiedene Funktionen.



In diesen Funktionen werden anstelle universeller Methoden zum Abrufen von Operanden bestimmte Methoden verwendet, die keine Prüfungen durchführen.

Hybrid Virtual Machine


Eine weitere Komplikation, die wir in Version 7.2 gemacht haben, ist die sogenannte hybride virtuelle Maschine.

Wenn wir den Handler früher immer über einen indirekten Aufruf direkt in der Interpreter-Schleife aufgerufen haben, haben wir jetzt für jeden Handler zusätzlich ein Label in den Rumpf der Schleife eingetragen, zu dem wir über den indirekten Sprung springen und den Handler selbst direkt aufrufen.



Früher schienen sie einen indirekten Anruf zu tätigen, jetzt zwei: einen indirekten Übergang und einen direkten Anruf, und ein solches System sollte langsamer arbeiten. Tatsächlich funktioniert es jedoch schneller, da wir dem Prozessor helfen, Übergänge vorherzusagen. Zuvor gab es einen Punkt, von dem aus der Übergang zu verschiedenen Orten durchgeführt wurde. Der Prozessor hat sich oft geirrt, weil er sich einfach nicht erinnern konnte, dass es notwendig war, zuerst auf einen Befehl zu springen, dann auf einen anderen. Nach jedem direkten Aufruf erfolgt nun ein indirekter Übergang zum nächsten Label. Wenn die PHP-Schleife ausgeführt wird, sind die virtuellen PHP-Anweisungen daher in stabilen Sequenzen angeordnet, die dann fast linear ausgeführt werden.

Die hybride virtuelle Maschine konnte die Produktivität um weitere 5-10% steigern.

PHP + OPcache + JIT


JIT wird als Teil von OPcache implementiert.



Nachdem der Bytecode kompiliert und optimiert wurde, wird ein JIT-Compiler dafür gestartet, der nicht mehr mit dem Quellcode funktioniert. Aus dem PHP-Bytecode generiert der JIT-Compiler nativen Code, wonach die Adresse des ersten Befehls (eigentlich die Funktion) im Bytecode geändert wird.

Danach wird der native, bereits generierte Code unverändert vom vorhandenen Interpreter aufgerufen. Ich zeige Ihnen ein einfaches Beispiel.



Links ist eine bestimmte Funktion in PHP geschrieben, die die Summe der Zahlen von 0 bis 100 zählt. Rechts der generierte Bytecode. Der erste Befehl weist der Summe 0 zu, der zweite macht dasselbe für i und dann einen unbedingten Sprung zum Label. Auf dem Etikett L1 wird die Bedingung zum Verlassen des Zyklus überprüft: Wenn sie erfüllt ist, verlassen Sie den Zyklus, wenn nicht, gehen Sie zum Zyklus. Addiere als nächstes die Summe i, schreibe das Ergebnis in den Betrag und erhöhe i um 1.

Direkt von hier aus generieren wir Assembler-Code, der sich als ziemlich gut herausstellt.



Die erste QM_ASSIGN Anweisung QM_ASSIGN in nur zwei Maschinenanweisungen (2-3 Zeilen) kompiliert. Das %esi Register enthält einen Zeiger auf den aktuellen Aktivierungsrahmen. Bei Versatz 30 liegt ein variabler Betrag. Der erste Befehl schreibt den Wert 0, der zweite 4 - dies ist ein Bezeichner eines Integer-Typs ( IS_LONG ). Für die Variable i Compiler erkannt, dass sie immer lang ist und es nicht erforderlich ist, den Typ zu speichern. Darüber hinaus kann es in einem Maschinenregister gespeichert werden. Daher ist hier einfach XOR des Registers bei sich die einfachste und billigste Anweisung zum Zurücksetzen.

Dann überprüfen wir auf die gleiche Weise, einen bedingungslosen Übergang, ob ein externes Ereignis aufgetreten ist, wir überprüfen den Zustand des Zyklus, wir gehen in den Zyklus. In der Schleife wird geprüft, ob die Summe eine Ganzzahl ist: Wenn ja, lesen wir den Ganzzahlwert, addieren den Wert i dazu, prüfen, ob ein Überlauf %edx , schreiben das Ergebnis zurück in die Summe und addieren 1 zu %edx .

Es ist zu sehen, dass der Code nahezu optimal ist. Es wäre möglich, es noch weiter zu optimieren und die Summe für den Typ bei jeder Iteration der Schleife nicht mehr zu überprüfen. Dies ist aber schon ziemlich umständlich, eine solche Optimierung machen wir noch nicht. Wir entwickeln JIT als relativ einfache Technologie . Wir versuchen nicht, das zu tun, was Java HotSpot versucht, V8 - wir haben weniger Leistung.

Was ist los mit jit


Warum können wir mit einem so guten Assembler-Code keine echten Anwendungen beschleunigen?

Eigentlich sollten sie?

  • Wenn der Engpass nicht in der CPU liegt, hilft JIT nicht.
  • Es wird zu viel Code generiert (Code bloat).
  • Statische Typinferenz funktioniert nicht immer.
  • Ehrlicher Code (für Fälle, die niemals ausgeführt werden).
  • Unterstützung für den konsistenten Status der virtuellen Maschine (und plötzlich eine Ausnahme).
  • Klassen leben nur für eine Anfrage.

Wenn die Anwendung 80% der Zeit auf eine Antwort aus der Datenbank wartet, hilft JIT nicht. Wenn wir externe ressourcenintensive Funktionen aufrufen, zum Beispiel Matching mit einem regulären Ausdruck, ruft JIT dieselben Funktionen auf dieselbe Weise auf. Wenn eine Anwendung große Datenstrukturen erstellt - Bäume, Diagramme und diese dann gelesen werden, generieren wir mit JIT Code, der weniger Anweisungen einliest. Das Laden der Daten selbst dauert jedoch genauso lange Sie müssen auch den Code laden.

Wie Sie bereits gesehen haben, kann JIT eine echte Anwendung sogar verlangsamen, da sie viel Code generiert und das Lesen zu einem Problem wird. Wenn Sie große Mengen Code lesen, werden andere Daten aus dem Cache entfernt, was zu einer Verlangsamung führt.

Bescheidene Pläne für PHP 8


Eine der Verbesserungen, die wir in PHP 8 erreichen wollen, ist, weniger Code zu generieren . Jetzt generieren wir, wie gesagt, systemeigenen Code für das gesamte Skript, den wir beim Laden laden. Aber die Hälfte der Funktionen wird sicherlich nicht aufgerufen. Also sind wir noch einen Schritt weiter gegangen und haben einen Auslöser eingeführt, mit dem wir konfigurieren können, wann JIT ausgeführt werden soll. Es kann ausgeführt werden:

  • für alle Funktionen;
  • Nur für Funktionen, wenn sie zum ersten Mal aufgerufen werden.
  • Sie können jeder Funktion einen Zähler hinzufügen und nur die Funktionen kompilieren, die wirklich aktuell sind.

Ein solches Schema funktioniert vielleicht etwas besser, ist aber immer noch nicht optimal, da es in jeder Funktion Pfade gibt, die ausgeführt werden, und Pfade, die niemals ausgeführt werden. Da PHP eine dynamische Programmiersprache ist, dh jede Variable unterschiedliche Typen haben kann, müssen Sie alle Typen unterstützen, die der statische Analysator vorhersagt. Und er tut dies oft mit Vorsicht, wenn er nicht beweisen konnte, dass der andere Typ es nicht konnte.

Unter diesen Umständen werden wir uns von der ehrlichen Zusammenstellung verabschieden und beginnen, dies spekulativ zu tun.



In Zukunft planen wir zunächst, die "heißesten" Funktionen für einige Zeit während der Arbeit der Anwendung zu analysieren, die Pfade des Programms zu untersuchen, welche Arten von Variablen es sind, vielleicht sogar die Randbedingungen zu berücksichtigen und erst dann den für den Strom optimalen Funktionscode zu generieren Ausführungsweise - nur für die Abschnitte, die tatsächlich ausgeführt werden.

Für alles andere werden wir Stubs setzen. Trotzdem wird es Überprüfungen und mögliche Ausgaben geben, bei denen der Deoptimierungsprozess beginnt, dh wir werden den für die Interpretation erforderlichen Zustand der virtuellen Maschine wiederherstellen und ihn dem Interpreter zur Ausführung übergeben.

Ein ähnliches Schema wird sowohl in HotSpot Java VM als auch in V8 verwendet. Die Anpassung der Technologie an PHP ist jedoch mit einer Reihe von Schwierigkeiten verbunden. Zunächst einmal haben wir Bytecode und nativen Code gemeinsam genutzt, die von verschiedenen Prozessen verwendet wurden. Wir können sie nicht direkt im gemeinsam genutzten Speicher ändern. Wir müssen sie zuerst kopieren, ändern und dann wieder in den gemeinsam genutzten Speicher übertragen.

Vorladen. Das Problem der Klassenbindung


Tatsächlich stammen viele der Ideen für PHP-Verbesserungen, die seit langem in PHP 7 und sogar in PHP 5 enthalten sind, aus JIT-bezogenen Arbeiten. Heute werde ich über eine andere derartige Technologie sprechen - dies ist das Vorladen. Diese Technologie ist bereits in PHP 7.4 enthalten und ermöglicht es, eine Reihe von Dateien anzugeben, diese beim Serverstart zu laden und alle Funktionen dieser Dateien permanent zu machen.

Eines der Probleme, die durch die Preloading-Technologie gelöst werden, ist das Problem der Klassenbindung. Tatsache ist, dass beim einfachen Kompilieren von Dateien in PHP jede Datei separat von den anderen kompiliert wird. Dies geschieht, weil jeder von ihnen separat geändert werden kann. Sie können eine Klasse aus einem Skript nicht mit einer Klasse aus einem anderen Skript verknüpfen, da sich bei der nächsten Anforderung möglicherweise eine Klasse ändert und ein Fehler auftritt. Darüber hinaus kann es in mehreren Dateien eine Klasse mit demselben Namen geben, und bei einer Anforderung wird eine von ihnen als übergeordnete Klasse verwendet, und bei der anderen wird eine andere Klasse aus einer anderen Datei verwendet (mit demselben Namen, aber mit einer völlig anderen). Es stellt sich heraus, dass Sie beim Generieren von Code, der für mehrere Anforderungen ausgeführt wird, nicht auf Klassen oder Methoden verweisen können, da diese jedes Mal neu erstellt werden (die Codelebensdauer überschreitet die Klassenlebensdauer).

Durch das Vorladen können Sie Klassen anfänglich binden und entsprechend den Code optimaler generieren. Zumindest für Frameworks, die mit Preloading geladen werden.

Diese Technologie hilft nicht nur bei der Klassenbindung. Ähnliches ist in Java als Class Data Sharing implementiert. Dort soll diese Technologie in erster Linie den Start von Anwendungen beschleunigen und den Gesamtspeicherbedarf reduzieren. Die gleichen Vorteile werden in PHP erzielt, da die Klassenbindung jetzt nicht mehr zur Laufzeit, sondern nur einmal ausgeführt wird. Außerdem werden die zugehörigen Klassen jetzt nicht im Adressraum jedes Prozesses, sondern im gemeinsam genutzten Speicher gespeichert, sodass der Gesamtspeicherverbrauch sinkt.

Die Verwendung von Preloading hilft auch bei der globalen Optimierung aller PHP-Skripte, beseitigt den OPcache-Overhead vollständig und ermöglicht Ihnen die Generierung von effizienterem JIT-Code.

Es gibt aber auch Nachteile. Beim Start geladene Skripte können nicht ersetzt werden, ohne PHP neu zu starten. Wenn wir etwas heruntergeladen und dauerhaft gemacht haben, können wir es nicht mehr entladen. Aus diesem Grund kann die Technologie mit stabilen Frameworks verwendet werden. Wenn Sie die Anwendung jedoch mehrmals täglich bereitstellen, funktioniert sie höchstwahrscheinlich nicht für Sie.

Die Technologie wurde als transparent konzipiert, das heißt, vorhandene Anwendungen (oder Teile davon) konnten unverändert geladen werden. Nach der Implementierung stellte sich jedoch heraus, dass dies nicht vollständig zutrifft. Nicht alle Anwendungen funktionieren wie beabsichtigt, wenn sie mithilfe von Preload geladen wurden . Wenn beispielsweise ein Code in der Anwendung basierend auf den Ergebnissen der Überprüfung der function_exists oder class_exists wird und die Funktion konstant wird, gibt function_exists immer true , und es wurde angenommen, dass der zuvor aufgerufene Code nicht aufgerufen wurde.

Technisch gesehen wird das Preloading mit nur einer Konfigurationsanweisung opcache.preload aktiviert, zu deren Eingabe Sie eine Skriptdatei angeben - eine reguläre PHP-Datei, die beim Start der Anwendung gestartet (nicht nur geladen, sondern ausgeführt) wird.

 <?php function _preload(string $preload, string $pattern = "/\.php$/") { if (is_file($path) && preg_match($pattern, $path)) { opcache_compile_file($path) or die("Preloading failed"); } else if (is_dir($path)) { if ($dh = opendir($path)) { while (($file = readdir($dh)) !== false) { if ($file !== "." && $file !== "..") { _preload($path . "/" . $file, $pattern); } } closedir($dh); } } } _preload("/usr/local/lib/ZendFramework"); 

Dies ist eines der möglichen Szenarien, in denen alle Dateien in einem bestimmten Verzeichnis (in diesem Fall ZendFramework) rekursiv gelesen werden. Sie können absolut jedes Skript in PHP implementieren: mit einer Liste lesen, Ausnahmen hinzufügen oder sogar mit Composer kreuzen, so dass es Podsoval-Dateien enthält, die zum Vorabladen benötigt werden. Das ist alles eine Frage der Technologie, und interessanter ist nicht, wie man versendet, sondern was man versendet.

Was beim Vorladen zu laden ist


Ich habe diese Technologie auf WordPress ausprobiert. Wenn Sie nur alle * .php-Dateien hochladen, funktioniert WordPress aufgrund der zuvor erwähnten Funktion nicht mehr: Es verfügt über eine Funktion_exists-Prüfung, die immer wahr wird. Daher musste ich das Skript aus dem vorherigen Beispiel leicht modifizieren (Ausnahmen hinzufügen), und dann funktionierte es ohne Änderungen in WordPress.

Geschwindigkeit [req / seq]Speicher [MB]Anzahl der SkripteAnzahl der FunktionenAnzahl der Klassen
Nichts3780000
Alle (fast *)3957.52541770148
Nur verwendete Skripte3964,584153251

Infolgedessen haben wir durch Vorspannung eine Beschleunigung von ~ 5% , was ohnehin nicht schlecht ist.

Ich habe fast alle Dateien heruntergeladen, aber die Hälfte davon wurde nicht verwendet. Sie können es noch besser machen - fahren Sie die Anwendung, und sehen Sie, welche Dateien heruntergeladen wurden. Sie können dies mit der Funktion opcache_get_status() tun, die alle zwischengespeicherten OPcache-Dateien zurückgibt und eine Liste für sie zum Vorabladen erstellt. Auf diese Weise können Sie 3 MB einsparen und etwas mehr Beschleunigung erzielen. Tatsache ist, dass je mehr Speicher benötigt wird, desto mehr der Prozessor-Cache verschmutzt und desto weniger effizient ist er. Je weniger Speicher verwendet wird, desto höher ist die Geschwindigkeit.

FFI - Foreign Function Interface


Eine weitere JIT-bezogene Technologie, die für PHP entwickelt wurde, ist FFI (Foreign Function Interface) oder auf Russisch die Fähigkeit, Funktionen, die in anderen kompilierten Programmiersprachen geschrieben wurden, ohne Kompilierung aufzurufen. Die Implementierung einer solchen Technologie in Python hat meinen Chef (Zeev Surazki) beeindruckt, und ich war sehr beeindruckt, als ich anfing, sie an PHP anzupassen.

Es gab bereits mehrere Versuche in PHP, eine Erweiterung für FFI zu erstellen, aber alle verwendeten ihre eigene Sprache oder API, um die Schnittstellen zu beschreiben. Ich habe die Idee in LuaJIT ausspioniert, wo die C-Sprache (eine Teilmenge) verwendet wird, um die Schnittstellen zu beschreiben, und das Ergebnis ist ein sehr cooles Spielzeug. Wenn ich jetzt überprüfen muss, wie etwas in C funktioniert, schreibe ich es in PHP - es passiert direkt in der Befehlszeile.

Mit FFI können Sie mit in C definierten Datenstrukturen arbeiten und in JIT integriert werden, um effizienteren Code zu generieren. Die libffi-basierte Implementierung ist bereits in PHP 7.4 enthalten.

Aber:

  • Dies sind 1000 neue Möglichkeiten, sich in den Fuß zu schießen.
  • Erfordert C-Kenntnisse und manchmal manuelle Speicherverwaltung.
  • Unterstützt C-Präprozessor (#include, #define, ...) und C ++ nicht.
  • Die Leistung ohne JIT ist ziemlich niedrig.

Obwohl, vielleicht für einige wird es bequem sein, weil der Compiler nicht benötigt wird. Dies funktioniert auch unter Windows ohne Visual-C von PHP.

Ich zeige Ihnen, wie Sie mit FFI eine echte GUI-Anwendung für Linux implementieren.

Lassen Sie sich vom C-Code nicht beunruhigen. Ich selbst habe vor ungefähr 20 Jahren eine grafische Benutzeroberfläche in C geschrieben, aber dieses Beispiel wurde im Internet gefunden.

 #include <gtk/gtk.h> static void activate(GtkApplication* app, gpointer user_data) { GtkWidget *window = gtk_application_window_new(app); gtk_window_set_title(GTK_WINDOW(window), "Hello from C"); gtk_window_set_default_size(GTK_WINDOW(window), 200, 200); gtk_widget_show_all(window); } int main() { int status; GtkApplication *app; app = gtk_application_new("org.gtk.example", G_APPLICATION_FLAGS_NONE); g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); status = g_application_run(G_APPLICATION(app), 0, NULL); g_object_unref(app); return status; } 

Das Programm erstellt die Anwendung, bleibt beim Rückrufaktivierungsereignis hängen und startet die Anwendung. Erstellen Sie im Rückruf ein Fenster, weisen Sie ihm die Titelgröße zu und zeigen Sie es an.

Und jetzt dasselbe in PHP umgeschrieben:

 <?php $ffi = FFI::cdef(" … // #include <gtk/gtk.h> ", "libgtk-3.so.0"); function activate($app, $user_data) { global $ffi; $window = $ffi->gtk_application_window_new($app); $ffi->gtk_window_set_title($window, "Hello from PHP"); $ffi->gtk_window_set_default_size($window, 200, 200); $ffi->gtk_widget_show_all($window); } $app = $ffi->gtk_application_new("org.gtk.example", 0); $ffi->g_signal_connect_data($app, "activate", "activate", NULL, NULL, 0); $ffi->g_application_run($app, 0, NULL); $ffi->g_object_unref($app); 

Hier wird zuerst das FFI-Objekt angelegt. Eine Beschreibung der Schnittstelle wird ihm als Eingabe gesendet - im Wesentlichen eine h-Datei - und die Bibliothek, die wir herunterladen möchten. Danach stehen alle in der Oberfläche beschriebenen Funktionen als Methoden des ffi-Objekts zur Verfügung und alle übergebenen Parameter werden automatisch und absolut transparent in die notwendige Maschinendarstellung übersetzt.

Es ist zu sehen, dass alles genau so ist wie im vorherigen Beispiel. Der einzige Unterschied besteht darin, dass wir in C einen Rückruf als Adresse gesendet haben und in PHP die Verbindung über den durch die Zeichenfolge angegebenen Namen hergestellt wird.

Nun wollen wir sehen, wie die Benutzeroberfläche aussieht. Im ersten Teil bestimmen wir die Typen und Funktionen in C und in der letzten Zeile laden wir die gemeinsam genutzte Bibliothek:

 <?php $ffi = FFI::cdef(" typedef struct _GtkApplication GtkApplication; typedef struct _GtkWidget GtkWidget; typedef void (*GCallback)(void*,void*); int g_application_run (GtkApplication *app, int argc, char **argv); unsigned long * g_signal_connect_data (void *ptr, const char *signal, GCallback handler, void *data, GCallback *destroy, int flags); void g_object_unref (void *ptr); GtkApplication * gtk_application_new (const char *app_id, int flags); GtkWidget * gtk_application_window_new (GtkApplication *app); void gtk_window_set_title (GtkWidget *win, const char *title); void gtk_window_set_default_size (GtkWidget *win, int width, int height); void gtk_widget_show_all (GtkWidget *win); ", "libgtk-3.so.0"); ... 

In diesem Fall werden diese C-Definitionen nahezu unverändert aus den h-Dateien der GTK-Bibliothek kopiert.

Um C und PHP in derselben Datei nicht zu beeinträchtigen, können Sie den gesamten C-Code in eine separate Datei packen, z. B. mit dem Namen gtk-ffi.h, und am Anfang ein paar spezielle define'ov hinzufügen, die den Namen der Schnittstelle und die zu ladende Bibliothek angeben:

 #define FFI_SCOPE "GTK" #define FFI_LIB "libgtk-3.so.0" 

Daher haben wir die gesamte Beschreibung der C-Schnittstelle in einer Datei ausgewählt. Diese gtk-ffi.h ist fast real, aber leider haben wir noch keinen C-Präprozessor implementiert, was bedeutet, dass Makros und Includes nicht funktionieren.

Laden wir nun diese Schnittstelle in PHP:

 <?php final class GTK { static private $ffi = null; public static function create_window($title) { if (is_null(self::$ffi)) self::$ffi = FFI::load(__DIR__ . "/gtk_ffi.h"); $app = self::$ffi->gtk_application_new("org.gtk.example", 0); self::$ffi->g_signal_connect_data($app, "activate", function($app, $data) use ($title) { $window = self::$ffi->gtk_application_window_new($app); self::$ffi->gtk_window_set_title($window, $title); self::$ffi->gtk_window_set_default_size($window, 200, 200); self::$ffi->gtk_widget_show_all($window); }, NULL, NULL, 0); self::$ffi->g_application_run($app, 0, NULL); self::$ffi->g_object_unref($app); } } 

Da FFI eine ziemlich gefährliche Technologie ist, möchten wir sie nicht an Dritte weitergeben. Lassen Sie uns zumindest das FFI-Objekt ausblenden, dh es innerhalb der Klasse als privat kennzeichnen. Und wir werden ein FFI-Objekt erstellen, das nicht FFI::cdef , sondern FFI::load und nur unsere h-Datei aus dem vorherigen Beispiel liest.

Der Rest des Codes hat sich nicht wesentlich geändert, nur als Event-Handler haben wir begonnen, eine unbenannte Funktion zu verwenden und den Titel mit lexikalischer Bindung weiterzugeben. Das heißt, wir verwenden sowohl C als auch die Stärken von PHP, die in C nicht verfügbar sind.

Eine auf diese Weise erstellte Bibliothek kann bereits in Ihrer Anwendung verwendet werden. Aber es ist gut, wenn es nur in der Befehlszeile funktioniert und wenn Sie es in den Webserver einfügen, wird die Datei gtk_ffi.h bei jeder Anforderung gelesen, eine Bibliothek erstellt und geladen, die Bindung wird durchgeführt ... Und all diese sich wiederholenden Arbeiten werden Ihren Server laden.

Um dies zu vermeiden und das Schreiben von PHP-Erweiterungen in PHP selbst zu ermöglichen, haben wir uns entschieden, FFI mit Preloading zu kreuzen.

FFI + Vorspannung


Der Code hat sich nicht wesentlich geändert, erst jetzt geben wir die h-Dateien zum Vorladen an und führen FFI::load direkt zum Zeitpunkt des Vorladens aus und nicht, wenn wir das Objekt erstellen. Das heißt, beim Laden der Bibliothek werden alle Analysen und Bindungen einmal ausgeführt (beim Serverstart), und mit Hilfe von FFI::scope("GTK") wir in unserem Skript Zugriff auf eine vorinstallierte Schnittstelle nach Namen.

 <?php FFI::load(__DIR__ . "/gtk_ffi.h"); final class GTK { static private $ffi = null; public static function create_window($title) { if (is_null(self::$ffi)) self::$ffi = FFI::scope("GTK"); $app = self::$ffi->gtk_application_new("org.gtk.example", 0); self::$ffi->g_signal_connect_data($app, "activate", function($app, $data) use ($title) { $window = self::$ffi->gtk_application_window_new($app); self::$ffi->gtk_window_set_title($window, $title); self::$ffi->gtk_window_set_default_size($window, 200, 200); self::$ffi->gtk_widget_show_all($window); }, NULL, NULL, 0); self::$ffi->g_application_run($app, 0, NULL); self::$ffi->g_object_unref($app); } } 

In dieser Ausführungsform kann FFI von einem Webserver verwendet werden. Dies gilt natürlich nicht für die GUI, aber auf diese Weise können Sie beispielsweise Bindungen an die Datenbank schreiben.

Eine auf diese Weise erstellte Erweiterung kann direkt über die Befehlszeile verwendet werden:
 $ php -d opcache.preload=gtk.php -r 'GTK::create_window(" !");' 

Ein weiteres Plus von FFI Crossbreeding und Preloading ist die Möglichkeit, die Verwendung von FFI für alle Skripte auf Benutzerebene zu verbieten. Sie können ffi.enable = preload angeben, was bedeutet, dass wir vorinstallierten Dateien vertrauen, aber das Aufrufen von FFI aus regulären PHP-Skripten ist verboten.

Arbeiten mit Datenstrukturen C


Ein weiteres interessantes Merkmal von FFI ist, dass es mit nativen Datenstrukturen arbeiten kann. Sie können jederzeit eine in C beschriebene Datenstruktur im Speicher anlegen.

 <?php $points = FFI::new("struct {int x,y;} [100]"); for ($x = 0; $x < count($points); $x++) { $points[$x]->x = $x; $points[$x]->y = $x * $x; } var_dump($points[25]->y); // 625 var_dump(FFI::sizeof($points)); // 800  foreach ($points as &$p) { $p->x += 10; } var_dump($points[25]->x); // 35 

Wir erstellen ein Array von 100 Strukturen (Anmerkung FFI :: new! = New FFI) mit zwei Ganzzahlen. , C. PHP, . count, / foreach . 800 , PHP PHP' , 10 .

FFI:

  • PHP.
  • PHP.

Python/CFFI : (Cario, JpegTran), (ffmpeg), (LibreOfficeKit), (SDL) (TensorFlow).

, FFI .

- PHP. , , callback' , . FFI. , . FFI c JIT, , LuaJIT, . , , .

 for ($k=0; $k<1000; $k++) { for ($i=$n-1; $i>=0; $i--) { $Y[$i] += $X[$i]; } } 

FFI .
Native ArraysFFI Arrays
PyPy0,0100,081
Python0,2120,343
LuaJIt -joff0,0370,412
LuaJit -jon0,0030,002
Php0,0400,093
PHP + JIT0,0160,087

: Zeev Surasky (Zend), Andi Gutmans (ex-Zend, Amazon), Xinchen Hui (ex-Weibo, ex-Zend, Lianjia), Nikita Popov (JetBrains), Anatol Belsky (Microsoft), Anthony Ferrara (ex-Google, Lingo Live), Joe Watkins, Mohammad Reza Haghighat (Intel) Intel, Andy Wingo (JS hacker, Igalia), Mike Pall ( LuaJIT).

, , .

PHP Russia 2020 ! telegram- , 2019 youtube- , , — .

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


All Articles