Ein Überblick über PHP Compiler und ihre Ausgaben von Nicolas Favre-Felix

Quelle: A review of PHP compilers and their outputs - By Nicolas Favre-Felix

Ein klasse Beitrag. Hier die Übersetzung:

Einleitung

Facebook generierte ganz schön viel Begeisterung als sie vor ein paar Wochen das Release ihres neuen PHP-Compilers ankündigten, HipHop-PHP. In diesem Artikel, werden wir bestehende PHP Optimierungs-Tools und was sie können um die Geschwindigkeit von PHP Seiten zu erhöhen besprechen. Das Release erhitzte erneut die Debatte darüber, ob Web-Applikationen durch die Geschwindigkeit von PHP oder durch die Geschwindigkeit ihrer Datenbank begrenzt sind; dieser Artikel handelt nur von Optimierungs-Werkzeugen für PHP Code.


Wir werden die folgenden Konzepte diskutieren:


Die Zend Engine, PHP opcodes

Die Zend Engine ist eine virtuelle Machine welche PHP Skripte ausführt. Es ist die offizielle Implementierung der PHP-Sprache. Diese virtuelle Maschine basiert auf Opcode: PHP Skripte sind in eine einfachere Sprache kompiliert, welche eine limitierte Anzahl von Operationen unterstützt. Zum Beispiel: Werte hinzufügen, eine Funktion aufrufen, Variablen vergleichen mit == sind sollche Operationen; ihre Opcodes sind ADD, DO_FCALL_BY_NAME, IS_EQUAL.

Wenn PHP-Skripte ausgeführt werden, passiert folgendes:

  1. Das Skript wird gelesen und in Tokens geteilt, welche in einen Parser gespeist werden
  2. Wenn das Skript valides PHP ist, wird Opcode generiert
  3. Die Zend Enginge führt den Opcode aus, indem es eine Funktion für jeden Opcode ausführt

Für eine detailliertere Erklärung dieses Prozesses, schaut rüber zu Sara Golemon’s Blog Post zum Verstehen von Opcode(Englisch).

Lasst uns den generierten Opcode anhand eines Beispiels untersuchen:

function fib($n) {

if($n === 0 || $n === 1) {
return $n;
}
return fib($n-1) + fib($n-2);
}

echo fib(30)."\n";

Dieses kleine Skript wird durch diesen ganzen Artikel hindurch verwendet, um den Wandel mit den verschiedenen Kompilierungs-Werkzeugen zu untersuchen.

Die Zend Engine generiert zwei Code-Blöcke: Einen für die fib Funktion und einen für den Top-Level-Call. Ich benutze die Vulcan Logic Dumper Erweiterung um den Opcode auszugeben, indem ich folgenden Befehl nutze: php -d vld.active=1 -d vld.execute=0 -f test.php

function name:  fib
number of ops: 20
compiled vars: !0 = $n
line # op fetch ext return operands
-------------------------------------------------------------------------------
29 0 RECV 1
31 1 IS_IDENTICAL ~0 !0, 0
2 JMPNZ_EX ~0 ~0, ->5
3 IS_IDENTICAL ~1 !0, 1
4 BOOL ~0 ~1
5 JMPZ ~0, ->8
32 6 RETURN !0
33 7* JMP ->8
34 8 INIT_FCALL_BY_NAME 'fib'
9 SUB ~2 !0, 1
10 SEND_VAL ~2
11 DO_FCALL_BY_NAME 1
12 INIT_FCALL_BY_NAME 'fib'
13 SUB ~4 !0, 2
14 SEND_VAL ~4
15 DO_FCALL_BY_NAME 1
16 ADD ~6 $3, $5
17 RETURN ~6
35 18* RETURN null
19* ZEND_HANDLE_EXCEPTION

function name:  (null)
number of ops: 7
compiled vars: none
line # op fetch ext return operands
-------------------------------------------------------------------------------
29 0 NOP
37 1 SEND_VAL 30
2 DO_FCALL 1 'fib'
3 CONCAT ~1 $0, '%0A'
4 ECHO ~1
40 5 RETURN 1
6* ZEND_HANDLE_EXCEPTION

Das sollte für jeden lesbar sein, der jemals Assembler-Sprache genutzt hat mit Registers und einem Stack. Ich bin jedoch verwundert über den/das NOP.


Opcode Cache

Jede HTTP-Anfrage liest und kompiliert die Datei erneut, bevor der Opcode ausgeführt wird. Für den Großteil der PHP Seiten, verbraucht das den verhältnismässig größten Anteil der gesamten Ausführungszeit, Opcode zu generieren, egal ob der Quellcode sich verändert hat oder nicht. Opcode Caches sind Plugins für die Zend Engine welche eine Kopie für den generierten Opcode nachdem er zum ersten mal gelesen wurde behält und umgeht dadurch das Parsing und Generieren bei den nächsten malen. Sie überprüfen lediglich ob sich die Datei seit dem letzten mal verändert hat und natürlich ist es ebenfalls möglich, diese Überprüfung auszustellen, um noch mehr Performance zu erreichen.

Die häufigsten Opcode Caches sind APC, XCache, eAccelerator. Eine volle Liste und Geschichte ist auf Wikipedia vorhanden. APC hat Beiträge von Facebook bekommen, wo es genutzt wurde. Facebook Entwickler haben ausserdem auf Webkonferenzen über APC tuning gesprochen.

Opcode Caches sind einfach zu nutzen und bringen oftmals "kostenlose" Performance ohne irgendeinen Code optimieren zu müssen.


PHP Erweiterungen: Wenn Opcodes Caching nicht genug ist


Falls ihr wirklich sicher seid, das der Flaschenhals die Ausführung des PHP Codes ist, gibt es einen Weg die Geschwindigkeit zu steigern, indem ihr manche Teile des PHP Codes in C umgeschreibt. Dieser C Code ist in eine .so Datei kompiliert, welche vom PHP Interpreter zur Laufzeit geladen wird; die kompilierten Module exportieren Funktionen und Klassen so, das PHP Skripte sie direkt nutzen können. Wann immer ihr einen Aufruf zum Memcache von PHP macht, benutzt ihr eine Erweiterung welche in C geschrieben ist.

Erweiterungen werden innerhalb PHP geladen und nutzen die internen Datenstrukturen und Schnittstellen(APIs) der Zend Engine. PHP Erweiterungen zu schreiben ist lästig: Es gibt nicht viele Dokumentationen, viele Funktionen sind Makros, sind inkonsistent oder verwirrend... Man macht eine schlechte Erfahrung damit...

Die Performance ist meistens von der Applikation abhängig
. Ich habe einen 60 fachen Geschwindigkeits-Zuschub bei bestimmten Funktionen: Die Möglichkeit zu haben Zeiger und benutzerdefinierte Datenstrukturen in C zu nutzen ist wesentlich effizienter als jedes mal den kompletten Code kopieren zu müssen, jedes mal wenn sich eine Variable ändert.

Das heißt, ihr könnt nicht einfach neuschreiben und erwarten das eure Funktionen besser laufen; Mögliche Flaschenhälse zu identifizieren und ein paar Kernfunktionen neu zu schreiben hilft aber dennoch.

PHP Erweiterungen mit PHC generieren
PHC ist ein PHP Compiler welcher von Paul Biggar geschrieben wurde, als Teil seines PhD. Es kann bestehenden PHP Code in C konvertieren, um als Erweiterung für PHP kompiliert zu sein. Die Idee ist, weiterhin dieselben Klassen und Funktionen aufzurufen, nur das sie dann schneller sein werden, weil sie in C geschrieben sind.

Es ist oft schwer dem Code den PHC generiert zu folgen und es gibt keinen einfachen Weg um es als C-Basis zu nutzen und ihn von Menschenhand zu warten.

Unsere fib Funktion hat eine 2500-Zeilen lange Datei generiert, die ersten 1350 sind PHC
Standardformulierungen welche vom Rest genutzt werden.

Eine generierte Kommentierung in der Ausgabe erklärt wie PHC den Code transformiert hat:

function fib($n)
{
$TLE2 = 0;
$TLE0 = ($n === $TLE2);
if (TLE0) goto L16 else goto L17;
L16:
$TEF1 = $TLE0;
goto L18;
L17:
$TLE3 = 1;
$TEF1 = ($n === $TLE3);
goto L18;
L18:
$TLE4 = (bool) $TEF1;
if (TLE4) goto L19 else goto L20;
L19:
return $n;
goto L21;
L20:
goto L21;
L21:
$TLE5 = 1;
$TLE6 = ($n - $TLE5);
$TLE7 = fib($TLE6);
$TLE8 = 2;
$TLE9 = ($n - $TLE8);
$TLE10 = fib($TLE9);
$TLE11 = ($TLE7 + $TLE10);
return $TLE11;
}

Hier ist ein Auszug aus dem generierten C-Code, entsprechend zu dem Kommentar weiter oben: (Ich habe die wichtigsten Punkte hervorgehoben; der Rest sind Standardformulierungen)

// $TLE11 = ($TLE7 + $TLE10);
{
if (local_TLE11 == NULL)
{
local_TLE11 = EG (uninitialized_zval_ptr);
local_TLE11->refcount++;
}
zval** p_lhs = &local;_TLE11;
zval* left;
if (local_TLE7 == NULL)
{
left = EG (uninitialized_zval_ptr);
}
else
{
left = local_TLE7;
}
zval* right;
if (local_TLE10 == NULL)
{
right = EG (uninitialized_zval_ptr);
}
else
{
right = local_TLE10;
}
if (in_copy_on_write (*p_lhs))
{
zval_ptr_dtor (p_lhs);
ALLOC_INIT_ZVAL (*p_lhs);
}
zval old = **p_lhs;
int result_is_operand = (*p_lhs == left || *p_lhs == right);
add_function(*p_lhs, left, right TSRMLS_CC);
if (!result_is_operand)
zval_dtor (&old;);
phc_check_invariants (TSRMLS_C);
}

Achtet auf den komischen Weg derAusführung, wahrscheinlich durch die fehlende Typen-Interference: Der PHC Compiler hat sein bestes gegeben um alle möglichen Aspekte abzudecken und das braucht Zeit.


Wie PHC den PHP’s “embed” Modus nutzt um ausführbare Dateien zu generieren


PHP hat verschiedene Interfaces, welche SAPIs (Server API) genannt werden. Existierende SAPIs enthalten apache, cli, cgi-fgci... Eine von ihnen unterstützt einen Weg um PHP Parser und virtuelle Maschine in C Programme einzubetten. PHC hat einen Vorteil von diesem Feature um die PHP Laufzeit zu bündeln zusammen mit dem generierten C-Code, während es eine ausführbare Binärdatei produziert.

Andere Compiler

Es gibt noch zwei andere Compiler, Roadsend-PHP und HipHop-PHP. Roadsend wurde neugeschrieben um LLVM zu nutzen, aber das Projekt ist anscheinend immernoch im Kindesalter, da die neue Version noch nicht implementiert ist. Diese Compiler sind anders als PHC, da sie nicht die Zend Laufzeit aber stattdessen ihr eigenes Ausführungs-System nutzen; beide sind fähig ausführbaren Dateien zu generieren.

Ein Blick auf Roadsend’s Ausgabe

Roadsend-PHP transformiert PHP Code in Scheme, eine funktionelle Sprache. Der Scheme Code ist dann in C konvertiert und kompiliert um eine ausführbare Binärdatei zu produzieren.

Hier ist die Ausgabe für fib:

(define test:test.php/fib
(lambda ($n)
#f
(push-stack 'unset 'fib $n)
(set! *PHP-LINE* 2)
(set! *PHP-FILE* "test.php")
(let ((ret1112
(begin
(begin0
(bind-exit
(return)
(let ()
#t
(begin
(if (or (identicalp $n #e0) (identicalp $n #e1))
(begin (return (copy-php-data $n)))
(begin))
(return
(php-+ (maybe-unbox
(begin
(set! *PHP-FILE* "test.php")
(set! *PHP-LINE* 7)
(let ((retval1110
(test:test.php/fib (php-- $n #e1))))
(set! *PHP-FILE* "test.php")
(set! *PHP-LINE* 7)
retval1110)))
(maybe-unbox
(begin
(set! *PHP-FILE* "test.php")
(set! *PHP-LINE* 7)
(let ((retval1111
(test:test.php/fib (php-- $n #e2))))
(set! *PHP-FILE* "test.php")
(set! *PHP-LINE* 7)
retval1111))))))
NULL))))))
(pop-stack)
ret1112)))

Ich habe den folgenden Befehl genutzt: pcc -v test.php -O --no-clean.

*PHP-FILE* und *PHP-LINE* sind globale Variablen welche aktualisiert werden, wenn der Code ausgeführt wird. Abgesehen von ein paar PHP-ähnlichen Funktionsaufrufen, ist der Code nach Scheme mit speziellen Operatoren für PHP Variablen aufgebaut. Das heißt, der generierte C-Code ist absolut unlesbar.
Zum Vergleich: auf meiner Maschine, fib(30) aufzurufen in PHP benötigt 0.95 Sekunden, während die von Roadsend generierte Binärdatei 0.44 Sekunden braucht. N.B. ist kein angemessener Vergleich.


Ein Blick auf HipHop’s Ausgabe

HipHop produziert folgende Ausgabe:

Variant f_fib(Numeric v_n) {
FUNCTION_INJECTION(fib);
if (same(v_n, 0LL) || same(v_n, 1LL)) {
return v_n;
}
return plus_rev(LINE(7,f_fib(v_n - 2LL)), f_fib(v_n - 1LL));
} /* function */

Das ist eine echte C++ Funktion welche ziemlich vergleichbar mit ihrem PHP Code ist.
Sie wird durch folgende Zeile aufgerufen:

echo(concat(toString(LINE(10,f_fib(30LL))), "\n"));

Das ist allerdings einfach zu hacken und einfach nicht verständlich.
Dieser Code lief auf meiner Maschine in der selben Zeit, wie der von Roadsends Compiler genierierte.
Das ist trotzdem kein fairer Vergleich, da das Programm in den Webserver eingebettet ist: Ein bedeutender Teil dieser Ausführung geht für das Setup und die Initialisierung drauf.


Fazit

Webentwickler haben viele Möglichkeiten wenn es Zeit ist, existierenden PHP-Code zu optimieren. Es gibt keinen Königsweg und der erste Schritt sollte immer sein, besseren Code zu schreiben; ein optimierender Compiler sichert nicht dein Blubble Sort-Verfahren. Die Big-O Notation zu nutzen ist ein guter Anfang, vereinfachte Daten-Strukturen anstatt von komplexen Hierachien von Objekten zu verwenden hilft oftmals auch gut.Wenn dein Code sauber ist, versuche Schritt für Schritt zu optimieren, ebenso wie deine Nutzerzahl wächst: Erst mit einem Opcode Cache und dann nutze einen Compiler erst, wenn du ihn wirklich brauchst. Chancen sind vorhanden, ein gut getunter APC wird schnell genug sein und es wird dir möglich sein, Dinge schnell ändern zu können, ohne die ganze Code-Basis neu kompilieren zu müssen.

Hinweise zu HipHop-PHP: Dieses Projekt sieht sehr vielversprechend aus aber ist noch sehr jung zur Zeit und trotzdem wird es von Facebook genutzt. Ich würde nicht empfehlen alles darauf laufen zu lassen bevor die Ecken und Kanten nicht weg sind.
Vergleichbare Bench
marks erscheinen bald in Foren und Blogs: Wenn du dich wirklich für eine technische Entscheidung auf sie verlässt, gehe sicher, dass sie die selben Dinge vergleichen. HipHop-PHP hat zum Beispiel seinen eigenen Webserver, und das hat Einfluss auf jeden HTTP-Benchmark. HTTP Benchmark: Vergleiche nicht Apache und libevent wenn du versuchst APC gegen HipHop zu vergleichen.

############

Ich finde diesen Beitrag echt klasse. Ich hoffe die Übersetzung ist nicht zu holprig, mir hat es leider ein wenig an Zeit gefehlt. Ich werde sie vielleicht bei Zeiten noch einmal überarbeiten.
Natürlich habe ich die schriftliche Erlaubnis den Text zu übersetzen und hier zu posten!

Übersetzt von Philipp Zentner.