
PHP is software written in C. The PHP codebase contains about 800 thousand lines of code and in the seventh version has been significantly reworked.
In this article we will look at what has changed in the Zend engine of the seventh version compared to the fifth one, and also consider how to effectively use internal optimizations. Take PHP 5.6 as a starting point. Often, much depends on how certain things are written and presented to the engine. When writing critical code, you need to pay attention to its performance. By changing a few little things, you can greatly speed up the engine, often without prejudice to other aspects like readability of the code or debug control. I will prove my reasoning with the Blackfire profiler.
If you want to improve PHP performance by migrating to version 7, you will need:
')
- Migrate the code base without making changes to it (or just convert it into code compatible with PHP 7). This will be enough to increase the speed of work.
- Use the tips below to understand how different parts of the PHP Virtual Machine code have changed and how to use them to further increase performance.
Packaged Arrays
Packed arrays are the first of the great optimizations in PHP 7. They consume less memory and in many cases work much faster than traditional arrays. Packed arrays must meet the criteria:
- Keys are integer values ​​only;
- Keys are inserted into the array only in ascending order.
Example 1:
$a = ['foo', 'bar', 'baz'];
Example 2:
$a = [12 => 'baz', 42 => 'bar', 67 => [] ];
These arrays are internally very well optimized. But it is obvious that on a three-cell array you will not feel a difference compared to PHP 5.
For example, in the symfony framework, packed arrays exist only in a class map generator, which creates code like:
array ( 0 => 'Symfony\\Bundle\\FrameworkBundle\\EventListener\\SessionListener', 1 => 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\NativeSessionStorage', 2 => 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\PhpBridgeSessionStorage', 3 => 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\NativeFileSessionHandler', 4 => 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Proxy\\AbstractProxy', 5 => 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Proxy\\SessionHandlerProxy', 6 => 'Symfony\\Component\\HttpFoundation\\Session\\Session',
If you execute this code on PHP 5 and PHP 7 and analyze using Blackfire, you get:

As you can see, the total compilation and execution time for this array declaration has decreased by about 72%. In PHP 7, such arrays become similar to NOPs, and in PHP 5, significant time is spent on compiling and loading during runtime.
Let's take a larger array, for 10,000 cells:
for ($i=0; $i<10000; $i++) { $a[] = $i; }
The same comparison:

Duration of processor usage has decreased about 10 times, and memory consumption has decreased from 3 MB to 0.5 MB.
The PHP development team has spent a lot of effort on optimizing arrays, because arrays are one of the main structures on which the language is based (the second most important structure is objects that are internally implemented using the same model).
The memory consumption of arrays in PHP 7 is much lower than in PHP 5. And when using packaged arrays, the savings are even higher.
Do not forget:
- If you need a list, then do not use strings in the keys (this will prevent the optimization of packed arrays);
- If the keys in the list are only integer, try to distribute them in ascending order (otherwise the optimization will not work either).
Such lists can be used in different parts of your application: for example, for enumeration (like a class map in Symfony) or for extracting results from a database in a specific order and with numeric data in a column. This is often necessary in web applications (
$pdo->query("SELECT * FROM table LIMIT 10000")->fetchAll(PDO::FETCH_NUM)
).
Integer and floating point values ​​in PHP 7 are free.
In PHP 7, a completely different way of placing variables in memory. Instead of heaps, they are now stored in stack memory pools. This has a side effect: you can reuse variable variable containers for free, no memory is allocated. This is not possible in PHP 5, there is a need to allocate some memory for each creation / assignment of a variable (which impairs performance).
Take a look at this code:
for ($i=0; $i<10000; $i++) { $$i = 'foo'; }
Here 10,000 variables are created with names from $ 0 to $ 10,000 with 'foo' as a string value. Of course, during the initial creation of a container variable (as in our example) some memory is consumed. But what if we now reuse these variables to store integer values?
for ($i=0; $i<10000; $i++) { $$i = 42; }
Here we simply reused the variables already allocated in memory. In PHP 5, this would require re-allocating memory for all 10,000 containers, and PHP 7 simply takes the finished ones and puts the number 42 in them, which does not affect the memory. In the seventh version, the use of integer and floating-point values ​​is completely free: the required size of memory is already allocated for the variable containers themselves.
Let's see what Blackfire says:

In PHP 7, rejecting additional memory access when a variable changes leads to savings in processor cycles in the second
for
loop. As a result, processor usage is reduced by 50%. And assigning an integer value further reduces memory consumption compared to PHP 5. In the fifth version, 80,000 bytes are spent on allocating 10,000 integers in memory (on the LP64 platform), as well as a bunch of additional memory per allocator. In PHP 7, these costs are not.
Optimization with encapsed strings
Encapsed strings are values ​​that internally scan for variables. They are declared using double quotes, or Heredoc syntax. The algorithm analyzes the value and separates the variables from the strings. For example:
$a = 'foo'; $b = 'bar'; $c = " $a $b";
When analyzing the line $ c, the engine should get the line: “I like foo and bar”. This process in PHP 7 has also been optimized.
This is what PHP 5 does:
- Allocates a buffer for "Like";
- Allocate buffer for "I like foo";
- Adds (copies in memory) to the last “Like” and “foo” buffer, returns its temporary contents;
- Allocates a new buffer for “I like foo and”;
- Adds (copies in memory) "I like foo" and "and" to this last buffer and returns its temporary contents;
- Allocates a new buffer for “I like foo and bar”;
- Adds (copies in memory) “I like foo and” and “bar” to this last buffer and returns its contents;
- Frees all intermediate used buffers;
- Returns the value of the last buffer.
Lots of work, right? Such an algorithm in PHP 5 is similar to what is used when working with strings in C. But the fact is that it does not scale well. This algorithm is not optimal when working with very long encapsed strings including a large number of variables. But encapsed strings are often used in PHP.
In PHP 7, everything works differently:
- A stack is created;
- It contains all the elements that need to be added;
- When the algorithm reaches the end of the encapsed line, memory of the necessary size is allocated at the same time, in which all parts of the data are moved to the right places.
Body movements with memory remained, however, no intermediate buffers, as in PHP 5, are already used. In PHP 7, memory is allocated only once for the final line, regardless of the number of parts of the line and variables.
Code and result:
$w = md5(rand()); $x = md5(rand()); $y = md5(rand()); $z = md5(rand()); $a = str_repeat('a', 1024); $b = str_repeat('a', 1024); for ($i=0; $i<1000; $i++) { $$i = " $a, $b, : $w - $x - $y - $z"; }
We created 1000 encapsed strings in which we find static string parts and six variables, two of which weigh 1 KB each.

As you can see, in PHP 7, processor utilization decreased by 10 times compared to PHP 5. Note that using the Blackfire Probe API (not shown in the example), we only profiled a loop, not the whole script.
In PHP 7:
$bar = 'bar'; $a = "foo $bar"; $a = "foo " . $bar;
Concatenation operation is not optimized. If you use string concatenation, you will end up doing the same strange things as in PHP 5. And encapsed strings will help you take advantage of the new analysis algorithm that performs estimated concatenation using the “
Rope “ structure.
Reference mismatch
Reference mismatch arises when you pass a non-reference variable (non-ref variable) to the function as an argument passed by reference (passed-by-ref), or vice versa. For example:
function foo(&$arg) { } $var = 'str'; foo($var);
Do you know what a nightmare is going on in the PHP 5 engine? If a link mismatch occurs, the engine has to duplicate the variable before passing it to the function as an argument. If the variable contains something large, like an array of several thousand records, then copying will take a lot of time.
The reason is in how PHP 5 works with variables and links. Moving to the body of the function, the engine still does not know whether you change the value of the argument. If you change it, passing the argument by reference should result in a reflection of the change made to the outside when passing the reference variable.
And if you do not change the value of the argument (as in our example)? Then the engine should create a link from a non-reference variable that you pass during the function call. In PHP 5, the engine completely duplicates the contents of a variable (with a very small number of pointers, calling
memcpy()
many times, which leads to a lot of slow memory accesses).
When the PHP 7 engine wants to create a link from a non-reference variable, it simply wraps it in a newly created former link (former). And no copying to memory. The thing is that in PHP 7, work with variables is built quite differently, and the links are significantly reworked.
Take a look at this code:
function bar(&$a) { $f = $a; } $var = range(1,1024); for ($i=0; $i<1000; $i++) { bar($var); }
Here is a double mismatch. When you call
bar()
you force the engine to create a link from
$var
to
$a
, as the
&$a
signature says. Since
$a
now part of the reference set (
$var-$a
) in the body of
bar()
, you can influence it with the value of
$f
: this is another mismatch. The link does not affect
$f
, so
$a-$f-$var
can not be connected to each other. However,
$var-$a
connected in one part, and
$f
is alone in the second part, until you force the engine to create copies. In PHP 7, you can quite easily create variables from links and turn them into links, only as a result copy-on-write can occur.

Remember that if you have not fully understood the links in PHP (and not many people can boast of this), then it is better not to use them at all.
We have seen that PHP 7 again saves a lot of resources compared to PHP 5. But in our examples we didn’t deal with copying cases when writing. Here everything would be different, and PHP would have to do a memory dump for our variable. However, using PHP 7 facilitates situations where mismatches may not be performed on purpose, for example, if you call
count($array)
, when part of the link is
$array
. In this case, there will be no additional expenses in PHP 7, but in PHP 5 the processor will become hot (with a sufficiently large array, for example, when collecting data from SQL queries).
Immutable arrays
The concept of immutable arrays appeared in PHP 7, they are part of the OPCache extension. An unchangeable is an array that is filled with immutable elements whose values ​​do not require calculations and become known at compile time: string values, integer values, floating point values, or containing all of the above arrays. In short, no dynamic elements and variables:
$ar = [ 'foo', 42, 'bar', [1, 2, 3], 9.87 ];
Immutable arrays were optimized in PHP 7. In PHP 5, no difference is made between arrays containing dynamic elements (
$vars
) and static when compiled. In PHP 7, immutable arrays are not copied, not duplicated, but remain read-only.
Example:
for ($i = 0; $i < 1000; $i++) { $var[] = [ 0 => 'Symfony\\Bundle\\FrameworkBundle\\EventListener\\SessionListener', 1 => 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\NativeSessionStorage', ]; }
This code creates one and the same array a thousand times (cut off, it can be represented as an array of hundreds or thousands of cells).
In PHP 5, such an array is duplicated in memory a thousand times. If it weighs several hundred kilobytes or even megabytes, then with a thousandfold duplication we will take up a large amount of memory.
In PHP 7, OPCache marks these arrays as immutable. The array is created once, and where necessary, a pointer to its memory is used, which leads to huge memory savings, especially if the array is large, as in the above example (taken from the Symfony 3 framework).
Let's see how performance changes:

Again, there is a huge difference between PHP 5 and PHP 7. PHP 5 needs to create an array 1000 times, which takes 27 MB of memory. In PHP 7 with OPCache only 36 Kb are involved!
If it is convenient for you to use immutable arrays, do not hesitate. Just do not violate the immutability by issuing a similar code:
$a = 'value'; $ar = ['foo', 'bar', 42, $a]; $ar = ['foo', 'bar', 42, 'value'];
Other considerations
We looked at some internal tricks to explain why PHP 7 is much faster than PHP 5. But for many workloads, this is micro-optimization. To get a noticeable effect from them, you need to use huge amounts of data or multiple cycles. This happens most often when PHP processes (workers) are started in the background instead of processing HTTP requests.
In such cases, you can greatly reduce the consumption of resources (processor and memory), just by writing code in a different way.
But do not believe your forebodings about possible performance optimizations. Do not blindly patch the code, use profilers to confirm or refute your hypotheses and check the actual performance of the code.