Introduction
Facebook generated a lot of buzz a few weeks ago when they announced the release of their new PHP compiler, HipHop-PHP. In this article, we will compare existing PHP optimization tools and what they can do to improve the speed of PHP pages. The release re-heated the debate over whether web applications are limited by the speed of PHP or the speed of their database; this article only deals with optimization tools for PHP code.
We will discuss the following concepts:
The Zend Engine, PHP opcodes
The Zend Engine is a virtual machine which runs PHP scripts. It is the official implementation of the PHP language. This virtual machine is opcode-based: PHP scripts are compiled into a simpler language which supports a limited number of operations, each with a code. For example: adding values, calling a function, comparing variables with == are such operations; their opcodes are ADD, DO_FCALL_BY_NAME, IS_EQUAL.
When a PHP script is executed, the following happens:
- The script is read and split into tokens, which are fed into a parser
- If the script is valid PHP, opcodes are generated
- The Zend Engine executes the opcodes, using a function for each opcode
For a more detailed explanation of this process, head over to Sara Golemon’s blog post on Understanding Opcodes.
Let’s examine the generated opcodes on a simple example:
function fib($n) {
if($n === 0 || $n === 1) {
return $n;
}
return fib($n-1) + fib($n-2);
}
echo fib(30)."\n";
This small script will be used throughout this article to examine its transformations by several compilation tools.
The Zend Engine generates two code blocks: one for the fib function, and one for the top-level call.
I used the Vulcan Logic Dumper extension to dump the opcodes, using the following command: 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
This should be readable by anyone who has ever used assembly language with registers and a stack. I wonder about the NOP, though.
Opcode caches
Each HTTP request re-reads and re-compiles the file before executing the opcodes. For the majority of PHP pages, each short-lived request spends a significant proportion of its total execution time parsing and generating opcodes whether the source file has changed or not. Opcode caches are plugins for the Zend Engine that keep a copy of the generated opcodes after the file is read for the first time, and bypass the parsing and generation steps after that. They will only check if the file has been modified, although it is even possible to remove that check in order to gain more performance.
The most common Opcode caches are APC, XCache, eAccelerator. A full list and history is available on Wikipedia. APC has received contributions from Facebook, where it is used. Facebook engineers have also given talks in Web conferences on APC tuning (link to a much recommended PDF slideshow).
Opcode caches are easy to use and often bring “free” performance without having to optimize any code.
PHP extensions: when caching opcodes is not enough
Once you’ve made sure the bottleneck is indeed the execution of PHP code, there is a way to improve the execution speed by re-writing parts of the PHP code in C. This C code is compiled into a .so file, which is loaded by the PHP interpreter at runtime; the compiled modules export functions and classes that PHP scripts can use directly. Whenever you make a call to memcache from PHP, you’re using an extension written in C.
Extensions are loaded inside PHP, and make use of the Zend Engine’s internal data structures and APIs. Writing PHP extensions is tedious: There isn’t a whole lot of documentation, many functions and macros are inconsistent or confusing… It is an unpleasant experience overall.
The performance gain mostly depends on the application. I’ve seen a speed-up of 60× on certain functions: being able to use pointers and custom data structures in C is much more efficient than having to copy a whole lot of data every time a variable changes.
That said, you can’t just rewrite and expect your functions to perform better; identifying possible bottlenecks and re-writing a few core functions worked for us.
Generating PHP extensions with PHC
PHC is a PHP compiler written by Paul Biggar as part of his PhD. It can convert existing PHP code into C, to be compiled as an extension for PHP. The idea is to keep calling the same classes and functions, only this time they’ll be faster because they’re written in C.
The code that PHC generates is often difficult to follow and there is no simple way to use it as a C base that could be maintained by a human being. Our fib function generated a 2500-line file, the first 1350 being PHC boilerplate used by the rest. A generated comment in the output explained how PHC transformed the code:
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;
}
Here is an excerpt of the generated C code, corresponding to the underlined statement above: (I have emphasized the important points; the rest is boilerplate)
// $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);
}
Note the weird flow of execution, probably due to the lack of type inference: The PHC compiler does its best to cover all possible issues and this take time.
How PHC uses PHP’s “embed” mode to generate executables
PHP has different interfaces, called SAPIs (Server API). Existing SAPIs include apache, cli, cgi-fgci… One of them provides a way to embed the PHP parser and virtual machine inside a C program. PHC takes advantage of this feature to bundle the PHP runtime along with the generated C code, producing an executable binary.
Other compilers
Two other compilers are available today, Roadsend-PHP and HipHop-PHP. Roadsend is being rewritten to use LLVM, but the project is apparently still in its infancy so the new version isn’t included here. These compilers are different from PHC as they don’t use the Zend runtime but provide their own execution system instead; both are capable of generating executable files.
A look at Roadsend’s output
Roadsend-PHP transforms PHP code into Scheme, a functional language. The Scheme code is then converted to C and compiled to produce an executable binary. Here is the output for 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)))
I used the following command: pcc -v test.php -O --no-clean.
*PHP-FILE* and *PHP-LINE* are global variables that are updated as the code is executed. Apart from a few PHP-related function calls, the code is pretty much straight Scheme code with special operators for PHP variables. That said, the generated C code is absolutely unreadable.
A point of comparison: on my machine, calling fib(30) in PHP takes 0.95 sec, while the roadsend-generated binary takes 0.44 sec. N.B. this is not a proper benchmark.
A look at HipHop’s output
HipHop produces the following output: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 */
This is a real C++ function that is very similar to the PHP code. It is called by the following line:
echo(concat(toString(LINE(10,f_fib(30LL))), "\n"));This is indeed hackable and understandable easily. This code ran on my machine in about the same time as the one generated by Roadsend’s compiler. This is still not a fair comparison, as the program embeds a whole web server: a significant part of this run must be spent in setup and initialization.
Conclusion
Web developers have many options when it comes to optimizing existing PHP code. There is no silver bullet and the first step should always be to write better code; an optimizing compiler won’t save your bubble sort. Using Big-O notation is a good start, using simple data structures instead of a complex hierarchy of objects will often help as well.
Once your code is clean, try optimizing it step by step to scale as your user base grows: first with an opcode cache, and then using a compiler if you actually need it. Chances are, a well-tuned APC will be fast enough and you’ll be able to deploy changes quickly without having to recompile the whole code base.
Notes on HipHop-PHP: This project looks very promising but is still very young at the moment, and although it is maintained by Facebook I wouldn’t recommend running anything on it before it gets rid of its rough edges.
Comparative benchmarks are going to start appearing soon on forums and blogs: If you rely on them to make a technical decision, make sure they actually compare the same things. HipHop-PHP has its own web server for example, and this has an influence on every HTTP benchmark: don’t compare Apache and libevent when you’re trying to benchmark APC against HipHop.
Comments
Here is the article translated into german:
http://www.allethemen.de/2010/02/re...
fyi for those reading this article, the 2 PHP compilers mentioned here are not the only ones out there; at least half a dozen are listed at Wikipedia (http://en.wikipedia.org/wiki/PHP#Co...), and some are far better than these two (depending on what you're trying to accomplish)
Hi. This article is quite informative and I found it interesting. I've also been to Paul Biggar's site and seen his articles.
I have a question : Does the PHC compiler directly compile PHP code, or does it compile intermediate Zend opcodes ? (much like Java compilation)
A response would be appreciated.
Thank you.
Hello Abhid-D.
The PHC compiler compiles PHP code into C code to compile as a PHP extension.
Nicolas
Thanks Nicolas for answering that.