Im Dezember 2015 wurde PHP 7.0 veröffentlicht. Unternehmen, die auf die "Sieben" umgestiegen sind, haben festgestellt, dass die Produktivität gestiegen ist und die Belastung des Servers gesunken ist. Die ersten, die zu den Sieben wechselten, waren Vebia und Etsy, und wir haben Badoo, Avito und OLX. Für Badoo kostete der Wechsel zu den sieben eine Million US-Dollar an Servereinsparungen. Dank PHP 7 in OLX konnte die durchschnittliche Serverlast um das Dreifache gesenkt, die Effizienz gesteigert und Ressourcen gespart werden.
Dmitry Stogov von Zend Technologies
sprach bei
HighLoad ++ , was die Produktivität steigerte. Bei der Dekodierung: über die interne Struktur von PHP, über die Ideen im Herzen von Version 7.0, über Änderungen in den grundlegenden Datenstrukturen und Algorithmen, die den Erfolg bestimmten.
Haftungsausschluss: Ab März 2019 laufen 80% der Websites mit PHP und 70% mit PHP 5, obwohl diese Version seit dem 1. Januar 2019 nicht mehr unterstützt wird . Der Bericht von Dmitry aus dem Jahr 2016 über die Grundsätze, aufgrund derer die Produktivität zwischen PHP 5 und 7 doppelt gestiegen ist, ist auch im März 2019 relevant. Für die Hälfte der Standorte sicher.Über den Sprecher: Dmitry Stogov begann bereits in den 80er Jahren mit der Programmierung: „Electronics B3-34“, Basic, Assembler. Im Jahr 2002 lernte Dmitry PHP kennen und begann bald daran zu arbeiten, es zu verbessern: Er entwickelte Turck MMCache für PHP, leitete das PHPNG-Projekt und spielte eine wichtige Rolle bei der Arbeit an JIT für PHP. Die letzten 14 Jahre als Principal Engineer bei Zend Technologies.
Darauf aufbauend entwickelt Zend Technologies PHP und kommerzielle Lösungen. 1999 wurde es von den israelischen Programmierern Andy Gutmans und Zeev Suraski gegründet, die vor zwei Jahren PHP 3 entwickelten. Diese Personen standen an der Spitze der PHP-Entwicklung und bestimmten maßgeblich das aktuelle Erscheinungsbild der Sprache und den Erfolg der Technologie.
Zend Technologies entwickelt den PHP-Kern und die Anwendungen dafür. Während der Arbeit musste ich Erweiterungen schreiben, in alle Subsysteme einsteigen und sogar kommerzielle Projekte durchführen, die manchmal überhaupt nicht mit PHP verbunden sind. Das interessanteste Thema für mich war jedoch immer die
Leistung .
Ich suchte nach Möglichkeiten, PHP zu beschleunigen, noch bevor ich zu Zend kam, und arbeitete an meinem eigenen Projekt, das mit dem Unternehmen konkurrierte. Während der Arbeit an dem Projekt habe ich die Sprache gründlich verstanden und festgestellt, dass Sie, wenn Sie nicht mit dem Mainstream-Projekt arbeiten, nur bestimmte Aspekte der Skriptausführung beeinflussen können und die interessantesten und effektivsten
nur im Kernel erstellt werden können . Dieses Verständnis und Zufall führte mich zu Zend.
Ein kleiner Exkurs in die Geschichte von PHP
PHP ist nicht nur und
nicht nur eine Programmiersprache . PHP steht für Personal Home Page - ein Tool zum Erstellen persönlicher Webseiten und dynamischer Websites. Die Sprache ist nur einer ihrer Hauptteile. PHP ist eine riesige Bibliothek von Funktionen, viele Erweiterungen für die Arbeit mit anderen Bibliotheken von Drittanbietern, zum Beispiel für den Zugriff auf die Datenbank oder XML-Parser, sowie eine Reihe von Modulen für die Kommunikation mit verschiedenen Webservern.
Der dänische Programmierer
Rasmus Lerdorf führte PHP
im Juni 1995 ein . Zu dieser Zeit war es nur eine
Sammlung von CGI-Skripten, die in Perl geschrieben wurden . Im April 96 führte Rasmus PHP / FI ein und im Juni wurde PHP / FI 2.0 veröffentlicht. Anschließend wurde diese Version von Andy Gutmans und Zeev Surasky grundlegend überarbeitet und im 98. PHP 3.0 veröffentlicht. Bis zum Jahr 2000 kam die Sprache zu der Art, wie wir sie heute sowohl in Bezug auf die Sprache als auch auf die interne Architektur gewohnt sind - PHP 4, basierend auf der Zend Engine.
Seit Version 4 hat sich PHP weiterentwickelt. Der Wendepunkt war die Veröffentlichung von PHP 5 im Jahr 2004, als das
Objektmodell vollständig aktualisiert wurde . Sie hat die Ära der PHP-Frameworks eröffnet und die Frage nach der Leistung auf ein neues Niveau gebracht. Unmittelbar nach der Veröffentlichung von 5.0 haben wir bei Zend darüber nachgedacht, PHP zu beschleunigen, und begonnen, an der Verbesserung der Produktivität zu arbeiten.
Die Version 7.1, die im November 2016 für synthetische Tests veröffentlicht wurde,
ist 25-mal schneller als die Version 2002 . Gemäß dem Diagramm der Leistungsänderungen in verschiedenen Zweigen sind die wichtigsten Durchbrüche in 5.1 und 7.0 sichtbar.

In Version 5.1 haben wir gerade angefangen, an der Leistung zu arbeiten, und alles, was wir übernommen haben - es stellte sich heraus, aber nach 5.3 stießen wir auf eine Wand, und alle Versuche, den Interpreter zu verbessern, scheiterten.
Trotzdem fanden wir heraus, wo wir graben konnten, und bekamen sogar mehr als erwartet - 2,5-fache Beschleunigung im Vergleich zur vorherigen Version 5.6 bei Tests. Das Interessanteste ist jedoch, dass wir bei unveränderten realen Anwendungen die gleiche 2,5-fache Beschleunigung erhalten haben. Dies ist ein Phänomen, da wir den vorherigen Faktor 2 während der gesamten Lebensdauer der fünf in 10 Jahren entwickelt haben.

Der enorme Sprung in 5.1 bei synthetischen Tests ist bei realen Anwendungen nicht spürbar. Der Grund dafür ist, dass bei unterschiedlichen Verwendungszwecken die PHP-Leistung auf den Bremsen beruht, die mit verschiedenen Subsystemen verbunden sind.
Die Geschichte von PHP 7 beginnt mit einer dreijährigen Stagnation , die 2012 begann und 2015 mit der Veröffentlichung der siebten Version endete. Dann stellten wir fest, dass wir mit kleinen Verbesserungen unseres Dolmetschers die Produktivität nicht mehr steigern konnten, und wandten uns der JIT-Seite zu.
JIT umherwandern
Fast zwei Jahre haben wir am JIT-Prototyp für PHP-5.5 gearbeitet. Zuerst haben wir einen sehr einfachen Code generiert - eine Folge von Aufrufen für Standard-Handler, so etwas wie einen zusammengefügten Fort-Code. Dann schrieben sie ihren eigenen
Runtime Assembler , einen separaten Inline-Code für Problemumgehungen, stellten jedoch fest, dass solche
Optimierungen auf niedriger Ebene selbst bei Tests
keine praktischen Auswirkungen hatten.
Dann haben wir darüber nachgedacht, Variablentypen mithilfe statischer Analysemethoden abzuleiten. Nachdem wir die Schlussfolgerung gezogen hatten, erhielten wir
in Tests sofort eine
zweifache Beschleunigung. Ermutigt versuchten sie, globale Registerzuordnungen zu schreiben, scheiterten jedoch. Wir haben eine ziemlich hochrangige Darstellung verwendet, und es war fast unmöglich, sie für die Registerzuordnung zu verwenden.
Um Probleme auf niedriger Ebene zu vermeiden, haben wir uns für LLVM entschieden und ein Jahr später eine
10-fache Beschleunigung für Bench.php erhalten , aber nichts für echte Anwendungen. Darüber hinaus dauerte das Kompilieren realer Anwendungen jetzt Minuten. Beispielsweise dauerte die erste
Anforderung an Wordpress 2 Minuten und führte zu keiner Beschleunigung. Dies war natürlich für die Praxis völlig ungeeignet.
Guter Code ist mit einer korrekten Typvorhersage möglich, die in realen Anwendungen schlecht funktioniert, und die Verwendung von PHP-Datenstrukturen macht den generierten Code ineffizient.
Was verlangsamt sich?
Wir haben die Gründe für die Fehler überdacht und uns erneut entschlossen zu sehen, warum PHP langsam ist. Das Bild zeigt das Ergebnis der Profilerstellung mehrerer Anforderungen an die Wordpress-Startseite.

Weniger als 30% werden für die Interpretation von Bytecode ausgegeben, 20% sind der Overhead des Speichermanagers, 13% arbeiten mit Hash-Tabellen und 5% arbeiten mit regulären Ausdrücken.
Bei JIT haben wir nur die ersten 30% losgeworden, und alles andere lag tot. Fast überall waren wir gezwungen, Standard-PHP-Datenstrukturen zu verwenden, was Overhead mit sich brachte: Speicherzuweisung, Referenzzählung usw. Dieses Verständnis führte zu der Schlussfolgerung, dass wichtige Datenstrukturen in PHP ersetzt werden müssen. Mit dieser
Ersetzung der Stiftung begann das
PHPNG- Projekt
.Phpng Neue Generation
Das Projekt wurde nach erfolglosen Versuchen entwickelt, JIT für PHP zu erstellen. Hauptziel ist
es, ein neues Produktivitätsniveau zu erreichen und den Grundstein für zukünftige Verbesserungen zu legen .
Wir haben uns für einige Zeit versprochen, keine synthetischen Tests mehr zur Messung der Leistung zu verwenden. Dies sind normalerweise kleine Computerprogramme, die eine begrenzte Datenmenge verwenden, die vollständig in den Cache des Prozessors passt. Im Gegensatz dazu unterliegen reale Anwendungen den mit dem Subsystemspeicher verbundenen Bremsen, und ein einzelnes Lesen aus dem Speicher kann 100 Rechenanweisungen kosten.
Das PHPNG-Projekt ist ein Refactoring der wichtigsten PHP-Datenstrukturen, um den Speicherzugriff zu optimieren . Keine Innovation, 100% PHP 5-kompatibel.
Wie man diese Strukturen ändert, war klar. Das Volumen der abhängigen Änderungen war jedoch enorm, da der
Kern von PHP selbst
150.000 Zeilen umfasst und fast jedes Drittel geändert werden musste. Fügen Sie hundert weitere Erweiterungen hinzu, die in der Basisverteilung enthalten sind, ein Dutzend Module für verschiedene Webserver, und Sie werden die Größe des Projekts erkennen.
Wir waren uns nicht einmal sicher, ob wir das Projekt abschließen würden. Daher starteten sie das Projekt im Geheimen und eröffneten es erst, als die ersten optimistischen Ergebnisse erschienen. Es dauerte zwei Wochen,
um den Kernel einfach zu
kompilieren . Zwei Wochen später verdient Bench.php. Wir haben anderthalb Monate damit verbracht, die Arbeit von Wordpress sicherzustellen. Einen Monat später haben wir das Projekt eröffnet - es war Mai 2014. Zu diesem Zeitpunkt hatten wir eine
Beschleunigung von 30% auf Wordpress . Es schien bereits ein großes Ereignis zu sein.
PHPNG erregte sofort eine Welle des Interesses und wurde im August 2014
als Grundlage für die Zukunft von PHP 7 angenommen . Es war bereits ein weiteres Projekt mit anderen Zielen, bei dem die Produktivität nur eines davon war.
PHP 7.0
Die Versionsnummer 7 selbst war zweifelhaft. Die vorherige Version war die fünfte. Die sechste wurde vor einigen Jahren entwickelt und war vollständig der nativen
Unicode- Unterstützung gewidmet. Die erfolglosen Entscheidungen in den frühen Entwicklungsstadien führten jedoch zu einer übermäßigen Komplexität des Kernel-Codes und jeder Erweiterung. Am Ende wurde beschlossen, das Projekt einzufrieren.
Zu diesem Zeitpunkt war bereits viel Material für PHP 6 angesammelt: Reden auf Konferenzen, veröffentlichte Bücher. Um niemanden zu verwirren, haben wir das Projekt PHP 7 genannt und PHP 6 übersprungen. Diese Version hatte viel mehr Glück - PHP 7 wurde im Dezember 2015 fast planmäßig veröffentlicht.
Neben der Leistung erschienen in PHP 7 einige lang ersehnte Innovationen:
- Möglichkeit, skalare Parametertypen und Rückgabewerte zu definieren.
- Ausnahmen statt Fehler - jetzt können wir sie abfangen und verarbeiten.
Zero-cost assert()
, anonyme Klassen, Reinigungsinkonsistenzen, neue Operatoren und Funktionen (<=>, ??) wurden angezeigt.
Innovation ist gut, aber zurück zu den internen Veränderungen. Lassen Sie uns über den Weg sprechen, den PHP 7 eingeschlagen hat und wohin uns dieser Weg führen kann.
zval
Dies ist die grundlegende PHP-Datenstruktur. Es wird verwendet,
um einen beliebigen Wert in PHP darzustellen . Da unsere Sprache dynamisch typisiert wird und sich der Variablentyp während der Programmausführung ändern kann, müssen wir ein Typfeld (Typ zend_uchar) speichern, das die Werte IS_NULL, IS_BOOL, IS_LONG, IS_DOUBLE, IS_ARRAY, IS_OBJECT usw. annehmen kann Der durch Union (Wert) dargestellte Wert, in dem eine Ganzzahl, eine reelle Zahl, eine Zeichenfolge, ein Array oder ein Objekt gespeichert werden kann.
zval in PHP 5
Der Speicher für jede solche Struktur wurde separat in Heap zugewiesen. Neben Typ und Wert wurde auch der Verweiszähler auf die Struktur darin gespeichert. Die Struktur benötigte also 24 Bytes, ohne den Overhead des Speichermanagers und den Zeiger darauf.
Das Bild oben rechts zeigt die Datenstrukturen, die im Speicher von PHP 5 für ein einfaches Skript erstellt wurden.

Auf dem Stapel wurde Speicher für 4 Variablen zugewiesen, die durch Zeiger dargestellt werden. Die Werte selbst (zval) befinden sich auf dem Heap. In unserem Fall sind dies nur zwei zval, auf die jeweils von zwei Variablen verwiesen wird, und dementsprechend werden ihre Referenzzähler auf 2 gesetzt.
Um auf einen Typ oder einen Skalarwert zuzugreifen, benötigen Sie mindestens zwei Messwerte: Lesen Sie zuerst den Wert des Zeigers und dann den Wert der Struktur. Wenn Sie keinen skalaren Wert lesen müssen, sondern beispielsweise einen Teil eines Strings oder Arrays, müssen Sie mindestens einen weiteren Messwert lesen.
zval in PHP 7
Wo wir vorher Zeiger verwendet haben, haben wir in den sieben begonnen, zval einzubetten. Wir haben uns von der Referenzzählung für Skalartypen entfernt. Der Feldtyp und der Wert blieben ohne wesentliche Änderungen, aber es wurden einige weitere Flags und ein reservierter Platz hinzugefügt, über die ich etwas später sprechen werde.

Links sieht es in PHP 5 aus und rechts in PHP 7.

Jetzt sind zval selbst auf dem Stapel. Zum Lesen von Typen und Skalarwerten reicht nur eine Maschinenanweisung aus. Alle Werte sind in einem Speicherbereich zusammengefasst. Dies bedeutet, dass wir bei der Arbeit mit lokalen Variablen praktisch keine Verluste aufgrund von Fehlern im Prozessor-Cache haben. Die wahre Leistung der neuen Leistung ist jedoch enthalten, wenn ein Kopiervorgang erforderlich ist.
Datensatz kopieren
In der obersten Zeile des Skripts wurde eine weitere Zuordnung hinzugefügt.

In PHP5 haben wir dem neuen zval Speicher aus dem Heap zugewiesen, sein int (2) initialisiert, den Wert des Zeigers auf die Variable b geändert und den Referenzzähler des Werts verringert, auf den sich b zuvor bezogen hatte.
In PHP 7 haben wir
die Variable b einfach
mit ein paar Anweisungen direkt an Ort und Stelle initialisiert , während in PHP 5 Hunderte von Anweisungen erforderlich waren. Also schaut zval jetzt in Erinnerung.

Dies sind zwei 64-Bit-Wörter. Das erste Wort
bedeutet: Ganzzahl, Real oder Zeiger. Im zweiten Wort der
Typ (es gibt an, wie die Bedeutung zu interpretieren ist), Flags und ein reservierter Platz, der beim Ausrichten noch hinzugefügt wird. Es verschwindet jedoch nicht, sondern wird von verschiedenen Subsystemen zum Speichern indirekt verwandter Werte verwendet.
Flags sind eine Reihe von Bits, wobei jedes Bit angibt, ob zval ein Protokoll unterstützt. Wenn es sich beispielsweise um
IS_TYPE_REFCOUNTED
handelt, sollte die Engine bei der Arbeit mit diesem zval auf den Wert des Referenzzählers achten. Beim Zuweisen erhöhen, beim Verlassen des Bereichs verringern, wenn der Referenzzähler Null erreicht, die abhängige Struktur zerstören.
Von den Typen erschienen im Vergleich zu PHP 5 mehrere neue.
IS_UNDEF
- ein Marker für eine nicht initialisierte Variable.- Das einzelne
IS_BOOL
durch separate IS_FALSE
und IS_TRUE
. - Ein separater Typ für Links und einige weitere magische Typen wurden hinzugefügt.
Typen von
IS_UNDEF
bis
IS_DOUBLE
sind skalar und benötigen keinen zusätzlichen Speicher. Um sie zu kopieren, reicht es aus, das 64-Bit-Wort des ersten Computers mit einem Wert und das halbe zweite mit einem Typ und Flags zu kopieren.
Nachgezählt
Bei anderen Typen schwieriger. Sie werden alle durch eine untergeordnete Struktur dargestellt, und zval speichert einfach einen Verweis auf diese Struktur. Für jeden Typ ist diese Struktur unterschiedlich, aber in Bezug auf OOP haben alle einen gemeinsamen abstrakten Vorfahren oder eine gemeinsame abstrakte Struktur zend_refcounted. Es bestimmt das Format des ersten
64-Bit-Wortes , in dem der Referenzzähler und andere Informationen für den Garbage Collector gespeichert sind.

Dieses Wort kann einfach als Information für den Garbage Collector betrachtet werden, und Strukturen für bestimmte Typen fügen ihre Felder nach diesem ersten Wort hinzu.
Linien
In den sieben für die Zeichenfolge speichern wir den berechneten Wert der Hash-Funktion, ihre Länge und die Zeichen selbst. Die Größe einer solchen Struktur ist variabel und hängt von der Länge der Zeichenfolge ab. Die Hash-Funktion wird bei Bedarf einmal für den String berechnet. In PHP 5 wurde es bei jedem Bedarf neu berechnet.

Jetzt sind die Zeichenfolgen referenzzählbar geworden. Wenn wir in PHP 5 die Zeichen selbst kopiert haben, reicht es jetzt aus, die Referenzanzahl für diese Struktur zu erhöhen.
Wie in PHP 5 haben wir immer noch das Konzept
unveränderlicher oder internierter Zeichenfolgen . Sie existieren normalerweise in einer Instanz, leben bis zum Ende der Abfrage und können sich wie skalare Werte verhalten. Wir müssen uns nicht um den Zähler der Links zu ihnen kümmern, und zum Kopieren reicht es aus, nur zval selbst mit vier Maschinenanweisungen zu kopieren.
Arrays
Arrays werden durch eine integrierte Hash-Tabelle dargestellt und unterscheiden sich nicht wesentlich von PHP 5. Die Hash-Tabelle selbst hat sich geändert, aber mehr dazu separat.

Arrays sind jetzt eine
adaptive Struktur , die ihre interne Struktur und ihr Verhalten in Abhängigkeit von den gespeicherten Daten geringfügig ändert. Wenn wir nur Elemente mit engen numerischen Schlüsseln speichern, erhalten wir direkten Zugriff auf die Elemente per Index mit einer Geschwindigkeit, die mit der Geschwindigkeit von Arrays in C vergleichbar ist. Wenn Sie jedoch ein Array mit einem Zeichenfolgenschlüssel zum selben Array hinzufügen, wird es zu einem echten Hash mit Kollisionsauflösung.
So sieht die Hash-Tabelle in PHP 5 aus.

Dies ist eine klassische Implementierung einer Hash-Tabelle mit Kollisionsauflösung unter Verwendung linearer Listen (in der oberen rechten Ecke gezeigt). Jeder Artikel wird durch einen Eimer dargestellt. Alle Buckets sind durch doppelt verknüpfte Listen verknüpft, um Kollisionen aufzulösen, und durch eine andere doppelt verknüpfte Liste, um die Reihenfolge zu durchlaufen. Die Werte für jedes zval werden separat zugeordnet - in Bucket speichern wir nur einen Link dazu. Zeichenfolgenschlüssel können auch separat zugewiesen werden.
Daher müssen Sie für jede Hash-Tabelle viele kleine Speicherblöcke zuweisen, und um später etwas zu finden, müssen Sie entlang der Zeiger laufen. Jeder solche Übergang kann zu einem Cahce-Miss und einer Verzögerung von ~ 10-100 Prozessorzyklen führen.
Dies ist, was in PHP 7 passiert ist.

Die logische Struktur blieb unverändert, nur die physische änderte sich. Unter einer Hash-Tabelle wird nun Speicher mit einer Operation zugewiesen.
Im Bild befinden sich am unteren Rand des Basiszeigers Elemente und am oberen Rand ein Hash-Array, das von einer Hash-Funktion angesprochen wird. Wenn wir bei flachen oder gepackten Arrays nur Elemente mit numerischen Indizes speichern, wird der obere Teil überhaupt nicht zugewiesen, und wir adressieren den Bucket direkt anhand der Nummer.
Um Elemente zu umgehen, sortieren wir sie nacheinander von oben nach unten oder von unten nach oben, was moderne Prozessoren fehlerfrei tun. Die Werte sind in Buckets integriert, aber der reservierte Speicherplatz in ihnen wird nur zum Auflösen von Kollisionen verwendet. Es speichert den Index eines anderen Buckets mit demselben Hash-Funktionswert oder dem Ende der Listenmarkierung.
Der Speicher für die Zeichenfolgenwerte der Schlüssel wird separat zugewiesen, aber es ist immer noch der gleiche zend_string. Beim Einfügen in ein Array reicht es aus, den Referenzzähler der Zeichenfolge zu erhöhen, obwohl wir zuvor die Zeichen direkt kopieren mussten und bei der Suche jetzt nicht die Zeichen, sondern die Zeiger auf die Zeichenfolgen selbst vergleichen können.
Unveränderliche Arrays
Früher hatten wir unveränderliche Zeichenfolgen, jetzt sind auch unveränderliche Arrays erschienen. Wie Zeichenfolgen verwenden sie nicht den Referenzzähler und werden erst am Ende der Anforderung zerstört. Dies ist ein einfaches Skript, das ein Array mit einer Million Elementen erstellt. Jedes Element ist dasselbe Array mit einem einzelnen "Hallo" -Element.

In PHP 5 wurde bei jeder Schleifeniteration ein neues leeres Array erstellt, "Hallo" wurde darauf geschrieben und all dies wurde dem resultierenden Array hinzugefügt. In PHP 7
erstellen wir in der Kompilierungsphase
nur ein unveränderliches Array , das sich wie ein Skalar verhält, und fügen es dem resultierenden hinzu. Im vorliegenden Beispiel können wir so den Speicherverbrauch um mehr als das Zehnfache und die Beschleunigung um das Zehnfache reduzieren.
Konstante Anordnungen von Millionen von Elementen in realen Anwendungen werden natürlich nicht oft gefunden, aber kleine sind ziemlich häufig. Auf jedem von ihnen erhalten Sie einen kleinen, aber einen Gewinn.
Die Objekte
Links zu allen Objekten in PHP 5 befanden sich in einem separaten Repository, und in zval gab es nur ein Handle - eine eindeutige Objekt-ID.

Um zum Objekt zu gelangen, haben wir mindestens 3 Messungen durchgeführt. Außerdem wurde der Speicher für den Wert jeder Eigenschaft des Objekts separat zugewiesen, und wir benötigten mindestens zwei weitere Lesungen, um es zu lesen.
In PHP 7 konnten wir zur direkten Adressierung übergehen.

Auf
zend_object
Adresse
zend_object
mit einer einzigen Maschinenanweisung
zend_object
. Und Property ist integriert und um sie zu lesen, benötigen Sie nur eine zusätzliche Lesung. Sie sind auch gruppiert, was
die Datenlokalität verbessert und modernen Prozessoren hilft, nicht zu stolpern.
Neben der vordefinierten Eigenschaft wird hier auch ein Link zur Klasse dieses Objekts gespeichert, einige Handler - ein Analogon zu virtuellen Methodentabellen und eine Hash-Tabelle für Eigenschaften, die nicht definiert wurden. In PHP können Sie jedem Objekt, das ursprünglich nicht definiert wurde, eine Eigenschaft hinzufügen. Wenn mehrere Maschinenanweisungen ausreichen, um auf die vordefinierte Eigenschaft zuzugreifen, müssen Sie für nicht vordefinierte Eigenschaften eine Hash-Tabelle verwenden, für die Dutzende von Maschinenanweisungen erforderlich sind. Das ist natürlich viel teurer.
Referenz
Schließlich mussten wir einen
separaten Typ einführen, um PHP-Links darzustellen.

Dies ist ein vollständig transparenter Typ. Es ist für PHP-Skripte nicht sichtbar. Skripte sehen ein anderes zval, das in die zend_reference-Struktur integriert ist. Es versteht sich, dass wir uns von mindestens zwei Stellen auf eine solche Struktur beziehen und der Referenzzähler dieser Struktur immer größer als 1 ist. Sobald der Zähler auf 1 fällt, verwandelt sich die Verknüpfung in einen regulären Skalarwert. Das in den Link eingebettete zval wird in das letzte zval kopiert, das darauf verweist, und die Struktur selbst wird gelöscht.
Es scheint, dass das Arbeiten mit Referenzen jetzt viel komplizierter ist als mit anderen Typen (und das ist wahr), aber tatsächlich mussten wir in PHP 5 Arbeiten von vergleichbarer Komplexität ausführen, wenn wir auf einen Wert zugreifen (sogar eine Primzahl). Jetzt wenden wir komplexere Protokolle nur auf einen Typ an und beschleunigen so die Arbeit mit allen anderen, insbesondere mit skalaren Werten.
IS_FALSE und IS_TRUE
Ich habe bereits gesagt, dass der einzelne Typ IS_BOOL in separate IS_FALSE und IS_TRUE aufgeteilt wurde. Diese Idee wurde in der LuaJIT-Implementierung ausspioniert und wurde entwickelt, um eine der häufigsten Operationen zu beschleunigen - den bedingten Übergang.

Wenn es in PHP 5 erforderlich war, den Typ zu lesen, nach Booleschen Werten zu suchen, den Wert zu lesen, herauszufinden, ob er wahr oder falsch ist, und darauf basierend einen Übergang vorzunehmen, reicht es jetzt aus, den Typ einfach zu überprüfen und mit true zu vergleichen:
- wenn es wahr ist, dann gehen wir entlang eines Zweigs;
- Wenn es weniger als wahr ist, gehen Sie zu einem anderen Zweig.
- Wenn es mehr als wahr ist, gehen Sie zum sogenannten langsamen Pfad (langsamer Pfad) und dort überprüfen wir, von welchem Typ es kam und was damit zu tun ist: Wenn es eine ganze Zahl ist, müssen wir seinen Wert mit 0 vergleichen, wenn float - wieder mit 0 ( aber echt) usw.
Aufruf Konvention
Eine Änderung der Aufrufkonvention oder der Funktionsaufrufkonvention ist eine wichtige Optimierung, die nicht nur Datenstrukturen, sondern auch zugrunde liegende Algorithmen betrifft. Im Bild links ist ein kleines Skript zu sehen, das aus der Funktion foo () und ihrem Aufruf besteht. Unten finden Sie den Bytecode, in den dieses Skript von PHP 5 kompiliert wurde.

Zuerst werde ich Ihnen sagen, wie es in PHP 5 funktioniert hat.
Calling Convention in PHP 5
Die erste
SEND_VAL
Anweisung bestand darin, den Wert "3" an die foo-Funktion zu senden. Zu diesem Zweck musste sie dem Heap ein neues zval zuweisen, den Wert (3) dort kopieren und den Wert des Zeigers auf diese Struktur auf den Stapel schreiben.

Ähnliches gilt für die zweite Anweisung. Weiter hat
DO_FCALL
CALL FRAME
initialisiert, einen Platz für lokale und temporäre Variablen reserviert und die Steuerung an die aufgerufene Funktion übertragen.

Die erste
RECV
überprüfte das erste Argument und initialisierte den Slot auf dem Stapel mit der entsprechenden lokalen Variablen ($ a). Hier haben wir auf das Kopieren verzichtet und einfach den Referenzzähler des entsprechenden Parameters (zval mit einem Wert von 3) erhöht. In ähnlicher Weise stellte die zweite
RECV
eine Verbindung zwischen der Variablen $ b und Parameter 5 her.

Weitere Körperfunktionen. 3 + 5 Addition geschah - es stellte sich heraus 8. Dies ist eine temporäre Variable und ihr Wert wurde direkt auf dem Stapel gespeichert.

RETURN und wir kehren von der Funktion zurück.

Bei der Rückgabe geben wir alle Variablen und Argumente frei, die außerhalb des Gültigkeitsbereichs liegen. Dazu gehen wir alle Zval durch, auf die durch Slots aus dem freigegebenen Frame verwiesen wird, und verringern für jeden den Referenzzähler. Wenn es 0 erreicht, zerstören Sie die entsprechende Struktur.
Wie Sie sehen können, erfordert selbst eine so einfache Operation wie das Senden einer Konstante an eine Funktion das Zuweisen von neuem Speicher, das Kopieren und Erhöhen des Referenzzählers sowie das doppelte Verringern und Löschen.
Calling Convention in PHP 7
In PHP 7 wurden diese Probleme behoben - jetzt speichern wir auf dem Stapel nicht die Zval-Zeiger, sondern die Zval-Zeiger selbst.

Wir haben auch eine neue Anweisung eingeführt,
INIT_FCALL
, die jetzt für die Initialisierung und Zuweisung von Speicher unter
CALL FRAME
ist und Platz für Argumente und temporäre Variablen reserviert.

SEND_VAL 3
kopiert das Argument jetzt nur noch in den ersten Slot nach dem
CALL FRAME
. Weiter
SEND_VAL 5
zum zweiten Slot.

Dann das interessanteste. Es scheint, dass
DO_FCALL
die Kontrolle an den ersten Befehl der aufgerufenen Funktion übergeben sollte. Aber die Argumente haben bereits die Slots getroffen, die für die variablen Parameter $ a und $ b reserviert sind, und die
RECV
Anweisungen tun einfach nichts. Daher können Sie sie einfach überspringen. Wir haben zwei Parameter gesendet, also überspringen wir zwei Anweisungen. Wenn sie drei geschickt hätten, hätten sie drei verpasst.

Also gehen wir direkt zum Hauptteil der Funktion, machen Addition und kehren zurück.

Bei der Rückkehr löschen wir alle lokalen Variablen, aber jetzt nur für zwei Slots, und da wir dort Skalare haben, müssen wir wieder nichts tun.

Meine Geschichte ist leicht vereinfacht, sie berücksichtigt keine Funktionen mit einer variablen Anzahl von Argumenten und die Notwendigkeit der Typprüfung und einige andere Punkte.
Die neue Calling Convention hat die Kompatibilität etwas gebrochen . PHP hat Funktionen wie
func_get_arg
und
func_get_args
. Wenn sie früher den ursprünglichen Wert des gesendeten Parameters zurückgegeben haben, geben sie jetzt den aktuellen Wert der entsprechenden lokalen Variablen zurück, da wir die ursprünglichen Werte einfach nicht speichern. Genau wie bei C. Debuggern

Außerdem kann die Funktion nicht mehr mehrere Parameter mit demselben Namen haben. Es hatte vorher keinen Sinn, aber ich traf solchen PHP-Code
foo($_, $_)
. Wie sieht es aus? (Ich erkannte Prolog)
Neuer Speichermanager
Nachdem wir die Optimierung der Datenstrukturen und der grundlegenden Algorithmen abgeschlossen hatten, machten wir erneut auf alle Bremssubsysteme aufmerksam. Der Speichermanager in PHP 5 nahm in Wordpress
fast 20% der Prozessorzeit in Anspruch.
Nachdem wir viele Zuweisungen beseitigt hatten, wurden seine Gemeinkosten geringer, aber immer noch erheblich - und zwar nicht, weil er bedeutende Arbeit leistete, sondern weil er über den Cache stolperte. Dies lag an der Tatsache, dass wir den klassischen Malloc-Algorithmus von Doug Lea verwendeten, bei dem geeignete freie Speicherorte durch Reisen durch Links und Bäume gefunden wurden, und all diese Fahrten zwangsläufig zu Cache-Fehlern führten.
Heute gibt es neue Speicherverwaltungsalgorithmen, die die Merkmale moderner Prozessoren berücksichtigen. Zum Beispiel:
jemalloc und
ptmalloc von Google . Zuerst haben wir versucht, sie unverändert zu verwenden, aber keinen Gewinn erzielt, da das Fehlen spezifischer PHP-Funktionen es am Ende der Anforderung teurer machte, Speicher vollständig freizugeben. Infolgedessen haben wir dlmalloc aufgegeben und etwas Eigenes geschrieben, indem wir Ideen aus dem alten Speichermanager und jemalloc kombiniert haben.
Wir haben
den Overhead von Memory Manager auf 5% reduziert, den Speicher-Overhead für Serviceinformationen reduziert und die Verwendung des CPU-Cache verbessert. Geeignete Speicherblöcke werden jetzt durch Bitmaps durchsucht, Speicher für kleine Blöcke wird von separaten Seiten zugewiesen und bei der Freigabe zwischengespeichert, spezielle Funktionen für häufig verwendete Blockgrößen werden hinzugefügt.
Viele kleine Verbesserungen
Ich habe nur über die wichtigsten Verbesserungen gesprochen, aber es gab viel kleinere. Ich kann einige davon erwähnen.
- Schnelle API zum Parsen von Parametern interner Funktionen und eine neue API zum Iterieren über HashTable.
- Neue VM-Anweisungen: Verkettung von Zeichenfolgen, Spezialisierung, Super-Anweisungen.
- Einige interne Funktionen wurden in VM-Anweisungen umgewandelt: strlen, is_int.
- Verwenden von CPU-Registern für VM-Register: IP und FP.
- Optimierung der Vervielfältigung und Löschung von Arrays.
- Verwenden Sie die Anzahl der Links, anstatt zu kopieren, wo immer Sie können.
- PCRE JIT.
- Optimierung interner Funktionen und Serialisierung ().
- Reduzierte Codegröße und verarbeitete Daten.
Einige waren sehr einfach, zum Beispiel waren nur drei Codezeilen erforderlich, um JIT in regulären Perl-Ausdrücken zu aktivieren, und dies brachte fast allen Anwendungen sofort eine sichtbare Beschleunigung (2-3%). Andere Optimierungen haben einige enge Aspekte bestimmter PHP-Funktionen berührt und sind nicht besonders interessant, obwohl der Gesamtbeitrag all dieser geringfügigen Verbesserungen ziemlich bedeutend ist.
Zu was bist du gekommen?
Dies ist der Beitrag verschiedener Subsysteme unter WordPress / PHP 7.0.

Der Beitrag der virtuellen Maschine stieg auf 50%. Memory Manager 5% — Memory Manager, . 130 . , 10 . , Memory Manager , .

:
- 2 .
- MM 17 .
- - 4 .
- WordPress 3,5 .
2,5- , . ? , , CPU time, — , . PHP , .
PHP 7
WordPress 3.6 — . - , PHP 7 mysql, , .

, PHPNG. 2/3 . , .
, WordPress, , — 1,5 2- .
PHP 7 HHVM
HHVM.

— . . Facebook . HHVM . , , , , .

PHP 7 — . Vebia, Etsy Badoo. Highload- , .
PHP 7.0 Etsy Badoo -. Badoo
.

, 2 , — 7 .
PHP 7.0.
, PHP 7.1, .
PHP Russia PHP 8 . PHP, , , — 1 . , , — , , , .