Last time, we looked at ways in which PHP 8.0 has become stricter. Today, we look at the next step in PHP becoming faster.
Computers don’t actually understand programming languages; they understand very low level instructions no human could write by hand. There are many ways of getting from a human-readable language like PHP or Rust to a computer-understandable set of instructions.
The most basic, and usually most performant, way is to compile the human-friendly source code directly to CPU instructions “Ahead-of-Time” (AOT). Those instructions then get saved to a “binary” stand-alone executable file. Some common languages that take this approach include C, C++, and Rust.
The least performant, but usually easiest to write, translation method is interpreted languages, often called “scripting languages.” In this case, there’s an “interpreter” program that translates each statement of source code into machine code as it’s “executed.” PHP 3 worked like that, which is why PHP 3 was so amazingly slow compared to modern PHP.
The latest trend in compilation is the introduction of a “Just in Time” compiler, or JIT. A JIT compiler starts with the simplified intermediary language, and rather than interpreting it, it converts it on-the-fly to machine code, stores that machine code in memory, and executes that.
JIT compilers are very tricky, because in order to get good performance out of them you generally need to be selective in which parts of the intermediary language are compiled to machine code and which aren’t. It’s not always faster to convert to machine code, depending on the specific details of the code and the language in question. Additionally, the process of converting the simplified code to native machine code may take longer than just running the simplified code once and being done with it.
For that reason, most JIT compilers analyze the code as it’s running to identify what parts would give the best bang for the buck and then compile just those bits. The net result, in theory, is that the program literally gets faster as it runs and as the JIT compiler in the virtual machine learns what parts of the code to bother optimizing.
PHP’s new JIT has been a long time coming. It’s actually been under development for several years and nearly shipped in an earlier form in PHP 7.4. Work toward making PHP JIT-capable was the impetus that led to the major rewrite of the engine that gave 7.0 its massive performance boost.
The PHP JIT is built as an extension to the opcode cache. That means it can be enabled and disabled either when building PHP itself or at runtime, via
The JIT extension is disabled by default. It can be enabled in
php.ini by setting
opcache.jit_buffer_size to a non-zero value. That controls how much space in memory the JIT can fill up with its optimized machine code. More is not always better, though, as the JIT could also waste time compiling code that doesn’t really benefit from being compiled.
The other main setting is
opcache.jit, which controls four levels of JIT aggressiveness. These levels are represented as 4-digit numbers, although they’re not actually numeric values but four different aggressiveness controls. The RFC and documentation have more details, so we won’t go into detail here.
There is no universally best configuration for the JIT. As is often the case with advanced tools like this, you’ll need to experiment with your own application and tune it appropriately.
But will the JIT improve performance? The predictable answer, as always, is “it depends.” For web apps, kinda maybe. For PHP as an ecosystem, immensely.
PHP, by design, usually runs in a shared-nothing configuration. After each request is handled, the program exits entirely. That gives the JIT very little time to analyze and optimize code, especially since most code in a typical web request is only executed once as the request is handled linearly. Besides, the largest part of those applications is often I/O (talking to the database, mainly), and the JIT can’t help with that at all. The benchmarks that have been published so far show the JIT offering only a marginal boost to performance in typical PHP applications run through PHP-FPM or Apache.
Where the JIT has the potential to be really helpful is in use cases where PHP is often not considered today. Persistent daemons, parsers, machine learning, and other long-running CPU intensive processes are where the real benefits lie. Much like the Foreign Function Interface (FFI) (part 1, part 2, part3) support added to PHP 7.4, the goal here is to allow PHP to break out of being a first-class web language and into a first-class general server language.
PHP-Parser is “a PHP parser written in PHP.” It’s from the same Nikita Popov we’ve been mentioning throughout this series and is used by many static analysis tools on the market today, such as PHPStan. Nikita has reported that PHP-Parser runs in some cases as much as twice as fast with the new JIT engine.
There are machine learning libraries available for PHP, such as Rubix ML or PHP-ML. They’re not as widely used as their Python equivalents, in part because, being interpreted, they tend to be slower than the C libraries with nice Python wrappers. With a JIT, however, these CPU-intensive tasks may end up being just as fast or possibly even faster as those available in other languages.
PHP is no longer just the fastest of the major web scripting languages. It’s now a viable high-performance general data processing language, which puts persistent workers, machine learning, and other high-CPU tasks into the hands of millions of existing PHP developers around the world.
We primarily have Dmitry Stogov and Zeev Suraski to thank for the multi-year effort to make this RFC happen.
What feature of PHP 8.0 is going to save you the most typing? Tune in next week to see what feature we’re … promoting.
You can try out pre-release copies of PHP 8.0 today on Platform.sh, with just a one-line change. Give it a whirl, and let us know what your favorite features are.