Optimizing PHP Code with Peachpie – Part 1


Optimizations are one of the great advantages of compilers and just-in-time compilers, and one of the key features of Peachpie. In this post we’ll show you that with Peachpie we achieve far more than “just” compatibility with a significant amount of existing PHP code and great possibilities of interoperating between PHP and .NET.

Background

It has been a while since we produced benchmarks and featured one of the many usecases of Peachpie, the PHP compiler to .NET. This article will be a combination of both – we will highlight one of the major advantages of Peachpie, the ability to optimize existing PHP codebases, and we will provide a plethora of quantitative analyses to compare standard PHP with Peachpie-optimized code.

The optimizations in this article are made possible by the latest analytics we implemented in Peachpie. These additions were discussed last week.

Test hardware

The following measurements and benchmarks were performed using an unmodified Lenovo Thinkpad X260 laptop, 8 GB RAM, Core i7-6500U CPU @ 2.50GHz (2 physical cores with hyperthreading), SSD, running Windows 10 x64 build 15063.413 with the latest .NET and Windows updates.

Out of the box optimizations

Peachpie completely revamps the approach to running PHP applications. Instead of interpreting the program each time, our solution is built on top of the Microsoft Roslyn compiler platform, the .NET standard runtime and an intensive type analysis. This results in a significant optimization of commonly performed tasks in PHP programs, taking advantage of years worth of experience with an already implemented and proven runtime.

Let’s outline the various areas where Peachpie can optimize existing PHP applications.

Parsing

The first task every PHP program has to do is to parse the source file. Despite the fact that it is mostly cached once it is performed, the parsing represents one of the more significant overheads of running PHP programs. Peachpie reads and parses source files during the compilation phase, i.e. already before the program is deployed and executed. To illustrate this point the following graph shows the times needed to process files of various sizes to demonstrate the amount of time saved for the program to start up:

parsetimes

The graph above depicts the average parse times in milliseconds according to the source code size in bytes. From the measurements we can see that on average, it takes about 8ms to process a file; the median value, however, amounted to right around 3ms. In case your PHP program consists of 10K files, the reading and parsing of the source files alone represents up to 30 seconds of CPU time overhead. On a multi-core CPU configuration, this is still noticeable.

With Peachpie, this processing is performed just once at compile time, i.e. before the actual program is being published and run on the server.

Include

PHP programs consist of source files linked together with include expressions. The inclusion defines the program flow on the file level. The include may point to any other or the same source file. In some cases, the compiler predicts what file will be included resulting in several other optimizations, most noticeably skipping the file resolution in runtime and allowing the just-in-time compiler to better optimize the program flow.

The code analysis recognizes a few patterns, which it is able then to resolve to a specific target file.
..?[/]w+
__DIR__" . "/w+
and others.

When inclusion matches the patterns above, they may be resolved in compile time instead of performing the file lookup in run time over and over.

This kind of optimization improves the runtime by about 1%.

Arithmetics

The complexity of arithmetic operations is caused by checking numeric types of its operands over and over. Peachpie defines the arithmetic operations for each common combination of operand types. With the help of type analysis and the compiler, the specific operation call is emitted without the need of handling type checking in runtime every time the operation has to be performed.

The optimization can improve the evaluation of expressions 10-50 times. Check out the diagram displaying the calculation of Pi using the Leibnitz formula:

Function call

One of the most important optimizations is the function call resolution. The compiler knows all the standard PHP functions. When we use the function in the PHP source code, it gets called directly, i.e. without any lookup in runtime. This drops the overhead from milliseconds to zero.

The following diagram compares Peachpie’s function calling using direct calls and callsites to standard PHP:

For and foreach

Loops are compiled as well and moreover, using the results of its type analysis, Peachpie is able to compile the construct in its most efficient way. A for loop in the form of i : integer; i < const integer; i++ is represented by the most efficient CIL instructions and further optimized by the .NET JIT (see below).

Let's take a look at how long the 'for' loop with 100 000 loops will take with and without optimizing the control variable:

As you can see, a for with 1000000 loops takes 0.26 sec without an optimized control variable, but a staggering 0.031 sec with one.

Hover over the control variables $i and $a in the two loops in the following snippet to see if they are optimized or not. The optimized variable is strictly typed as integer. Not optimized variables will have various types across the code flow or the type mixed.

In case of a foreach, the type analysis may infer the type of enumerator (array or Iterator or .NET IEnumerable). Subsequent calls to MoveNext and Current are then dispatched directly with no overhead.

JIT

The big advantage of a compilation into .NET byte code is the use of just in time compilation. The process is continuously developed and improved by Microsoft and all the .NET community optimizing the intermediate code (CIL) for "your" actual hardware, OS and available CPU instruction set. In this phase, method calls and stack manipulation are further inlined and actual machine code is generated.

Constant propagation

The compiler keeps track of global constants and class constants. In case of an environment-neutral constant, it is inlined in compile time. This saves the constant resolution in runtime and allows for more optimizations. The most significant one is the constant propagation made by the compiler and the .NET JIT itself.

This kind of optimization, when properly used, may save 100% of computing time. Usually, the original PHP performs a lookup into hashtables in order to resolve the constant value. This operation may be performed without any overhead with Peachpie.

In Part 2 of this article, we will take a more in-depth look at the code analysis and outline some additional optimizations that can be conducted thanks to Peachpie.

Posted on June 21, 2017, in category Benchmark, Information, tags: , , , , , , ,