Accelerating PHP Performance
OPCache and JIT. Essential for complex applications beyond basic CRUD.
Introduction
As is well known, there are two basic types of programming languages: compiled and interpreted. As their names suggest, the code of compiled languages must be compiled into binary code before execution, which is directly read by the processor. In contrast, interpreted languages have their code read and translated into operation code (hereafter referred to as opcode), which is then translated into binary code for the processor to execute.
Because the interpreter must first read the file to be executed, interpret its code, load dependencies, translate into opcodes, and then have the Zend virtual machine process the opcodes before finally passing them to the processor, interpreted languages are considered slow due to these extra operations.
This was the case until it was realized that on production servers, the source code of applications changes infrequently, and repeatedly reading and processing the same files is a waste of time. Instead, files can be read once and kept in memory in a compiled form, avoiding recompilation with each request.
This led to the development of the OPCache extension.
PHP Script Lifecycle
If we follow the steps of script execution from the moment it is called in PHP, it looks like this: Parsing > Compilation > Execution.
However, if we add the OPCache extension, two more steps are added to the script lifecycle between compilation and execution: Optimization and Caching.
Thus, with OPCache, the PHP script lifecycle looks like this: Parsing > Compilation > Optimization > Caching > Execution.
Parsing
This is the first step in the script lifecycle. At this stage, the parser and lexer break the code into tokens and pass them to the compiler. This process is called tokenization and helps the compiler understand the interrelation of tokens.
Compilation
At this step, the received tokens are transformed into opcodes, which contain instructions for executing the application.
Optimization
Due to PHP's characteristics, cross-file optimization is not possible. Thus, optimization occurs only at the level of a single file.
Intra-file optimization can optimize known values in advance. For example, if we have a file named script.php located in the /tmp directory, the following script will be optimized:
if (__DIR__ == '/tmp') {
echo 'yes';
} else {
echo 'no';
}
Since OPCache initially knows where the file to be optimized is located, there is no need to check its location within the code again, and the compiler will reduce this to a simple
// Если на этапе компиляции файл находился в директории /tmp
echo 'yes';
// В иных случаях
echo 'no';
Here are a few more examples:
if ($a) {
echo 'foo';
} else {
echo 'bar';
}
Here, the compiler cannot predict in advance what value will be in the variable $a and will leave the check for execution time.
if function_exists('my_custom_function')) { }
In this case, the compiler also cannot optimize the code because the function my_custom_function is not in the standard namespace. The compiler does not optimize checks for the existence of user-defined functions, as they may be in other files.
if (function_exists('array_merge')) {
echo 'yes';
}
However, the compiler optimizes checks for the existence of standard functions, as it can verify their presence in the standard namespace, and the result of this code is known in advance.
The same applies to constants. Since constants never change, the compiler, instead of calling them by pointer in memory each time, simply substitutes their value directly into the necessary places in the code.
Thus, the following code
const SOME_STRING = 'foo';
echo SOME_STRING;
Will become a direct output of the value:
echo 'foo';
Caching
At this step, OPCache allocates shared memory for storing scripts. It is important to understand that this memory segment is allocated on a permanent basis, meaning it will neither be changed nor removed from memory.
The size of this memory segment is specified in the opcache.memory_consumption setting. Of course, there is no universal recommendation for setting the size of allocated memory, as different applications have different requirements. However, the larger the application, the more memory it needs, which is logical. Don't forget that framework files will also be compiled and placed in shared memory, so it's better to allocate as much memory as possible for the shared segment.
But that's for large projects. For smaller ones, like this blog you're reading, 128MB is sufficient, with only about 50MB used.
So, what does OPCache store in this memory? Primarily, as you might guess, immutable data is stored in shared memory. This can include compiled classes, functions, and constants. Thus, all necessary data is stored in memory and accessed via a pointer, without the need for recompilation or object copying.
OPCache loads the script, compiles it, calculates the exact memory size needed to store the compiled data, and places it there. Thus, the memory is never freed or fragmented; once an object is placed, it remains accessible in memory at its address forever. Returning to the memory allocation setting, remember that the necessary memory is not the size of the code file but the number of objects and variables it creates.
JIT Compilation
Wait! But there was already compilation mentioned above, why talk about it again?
Yes and no. JIT compilation occurs below the usual compilation of files into opcodes, as it translates the previously compiled opcodes directly into machine code, into processor instructions, bypassing PHP's virtual machine.
Not every file or even every block of code will be compiled using JIT. This type of compilation can significantly speed up the logical part of the code. It allows for faster array operations, byte manipulations, image processing, and even opens up the possibility of developing Machine Learning in PHP! However, JIT compilation does not speed up input-output functions, as disk and network speeds have their limitations.
JIT compilation, like any other tool, should be used when you understand its necessity. It won't speed up an average blog, as a blog's work is mostly CRUD operations.
Conclusion
So, do we need OPCache and JIT? Undoubtedly, yes! If an application is not tied to input-output operations and is used, for example, for complex calculations, compiled code will work significantly faster than interpreted code.
From personal experience, I can say that searching for overlapping areas became significantly faster after enabling JIT, as it is purely logical data processing.
However, of course, no compiler can speed up poorly written code. So, trust the optimizer, but don't slack off!