PHP Opcode: Improve Application Performance Without Changing Your Code

The PHP opcode generated by the PHP engine is strongly influenced by the way you write your code, not only in terms of the number of statements to accomplish a task. Clearly, it matters a lot, and I think it’s obvious to you.

What could be less obvious is that even the syntax of the code can completely change the generated opcode causing a lot of overhead for the machine’s CPU to execute the exact same code.

In the last few years, my SaaS product has grown a lot, and it has given me the opportunity to go deeper and deeper into optimization techniques to run my workload as efficiently as possible.

The results I saw are impressive and helped me a lot in unlocking free cash flow to continue developing my SaaS journey.

At this point, the PHP process inside my SaaS product is processing more than 1.2 billion (with a "b") data packets every day on a machine with 2vCPU and 8GB of memory.  I use an AWS autoscaling group to have more flexibility in case of unpredictable spikes, but it rarely adds a second machine (one/two times a week).

Let’s go into the topic of the article. I think you will find it very interesting.

What Is PHP Opcode?

PHP opcode stands for operation code, and it refers to the low-level instructions that are executed by the PHP engine after the PHP source code you write has been compiled. 

In PHP, code compilation happens at runtime: basically, the first time that your code is taken by the PHP engine it will be compiled into this machine-friendly code, cached  (so the engine doesn’t compile the same code again), and then executed.

This is a simple representation of the process:

PHP Opcode process representation

PHP Opcode Caching

Caching the PHP opcode allows you to save three steps in the process of executing the code: parsing the raw PHP code, tokenization, and compilation.

Once the opcode is generated for the first time, it is stored in memory so it can be reused in subsequent requests. This reduces the need for the PHP engine to recompile the same PHP code every time it’s executed, saving a lot of CPU and memory consumption.

The most commonly used opcode cache in PHP is OPCache, and it is included by default from PHP 5.5 up to recent versions. It is highly efficient and widely supported.

Caching the precompiled script bytecode requires invalidating the cache after every deployment. This is because if changed files have the bytecode version in the cache, PHP will continue to run the old version of the code until you purge the opcode cache, so the new code will be compiled again generating a new cache item.

How to Investigate PHP Opcode

To understand how different syntax can impact the script’s opcode we need a way to grab the compiled code generated by the PHP engine.

There are two ways of getting the opcode.

OPCache Native Functions

If you have the OPCache extension enabled on your machine, you can use its native functions to get the opcode of a specific PHP file:

PHP
 
// Force compilation of a script
opcache_compile_file(__DIR__.'/yourscript.php');

// Get OPcache status
$status = opcache_get_status();

// Inspect the script's entry in the cache
print_r($status['scripts'][__DIR__.'/yourscript.php']);


VLD (Vulcan Logic Disassembler) PHP Extension

VLD is a popular PHP extension that disassembles compiled PHP code and outputs the opcode. It’s a powerful tool for understanding how PHP interprets and executes your code. Once installed, you can run a PHP script with VLD enabled by using the php command with -d options:

Shell
 
php -d vld.active=1 -d vld.execute=0 yourscript.php


The output will include detailed information about the compiled opcode, including each operation, its associated line of code, and more.

Use 3v4l (Acronym for EVAL)

3v4l is a very useful online tool that allows you to view the opcode generated by a PHP code you type into the editor. It basically is a PHP server with VLD installed so it can grab the VLD output and show you the opcode in the browser.

As it’s free, we’ll use this online tool for the next analyses.

How to Generate Efficient PHP Opcode

3v4l is perfect for understanding how the code syntax we use can influence the resulting PHP opcode in a good or bad way. Let’s start pasting the code below into 3v4l. Keep the configuration “all supported versions” and click on “eval”.

PHP
 
<?php

namespace App;

strlen('ciao');


After executing the code, a tab menu will appear on the bottom. Navigate to the VLD tab to visualize the correspondent Opcode. 

Shell
 
line      #* E I O op                           fetch          ext  return  operands
-------------------------------------------------------------------------------------
    5     0  E >   INIT_NS_FCALL_BY_NAME                                    'App%5CSpace%5Cstrlen'
          1        SEND_VAL_EX                                              'ciao'
          2        DO_FCALL                                      0          
          3      > RETURN                                                   1


Note that the first operation is INIT_NS_FCALL_BY_NAME. The interpreter constructs the name of the function using the namespace of the current file, but it doesn’t exist in the App\Example namespace — so how does it work?

The interpreter will check if the function exists in the current namespace. If it doesn’t, it tries to call the corresponding core function.

Here we have the opportunity to tell the interpreter to avoid this double check and directly execute the core function.

Try to add a backslash (\) before strlen and click “eval”:

PHP
 
<?php

namespace App;

\strlen('ciao');


In the VLD tab, you can now see the opcode with just one statement.

line      #* E I O op                           fetch          ext  return  operands
-------------------------------------------------------------------------------------    5     0  E > > RETURN                                                   1


Because you communicated the exact location of the function, it doesn’t need to consider any fallback.

If you don’t like to use the backslash, you can import the function like any other class from the root namespace:

PHP
 
<?php

namespace App;

use function strlen;

strlen('ciao');


Leverage Automatic Opcode Optimizations

There are also a lot of internal automatisms of the PHP engine to generate an optimized opcode evaluating static expressions in advance. This was one of the most important reasons for the great performance improvement of PHP since version 7.x.

Being aware of these dynamics can really help you reduce resource consumption and cut costs. Once I did this research, I started using these tricks throughout the code.

Let me show you an example using PHP constants. Run this script into 3v4l:

PHP
 
<?php

namespace App;

if (PHP_OS === 'Linux') {
    echo "Linux";
}


Take a look at the first two lines of the PHP opcode:

line      #* E I O op                           fetch          ext  return  operands
-------------------------------------------------------------------------------------    5     0  E >   FETCH_CONSTANT                                   ~0      'App%5CPHP_OS'          1        IS_IDENTICAL                                             ~0, 'Linux'          2      > JMPZ                                                     ~1, ->4    6     3    >   ECHO                                                     'Linux'    7     4    > > RETURN                                                   1


FETCH_CONSTANT tries to get the value of PHP_OS from the current namespace and it will look into the global namespace as it doesn’t exist here. Then, the IS_IDENTICAL instruction executes the IF statement.

Now try adding the backslash to a constant:

PHP
 
<?php

namespace App;

if (\PHP_OS === 'Linux') {
    echo "Linux";
}


As you can see in the opcode, the engine doesn’t need to try to fetch the constant because now it’s clear where it is, and since it’s a static value it already has it in memory.

Also, the IF statement disappeared because the other side of the IS_IDENTITCAL statement is a static string (‘Linux’) so the IF can be marked as “true” without the overhead of interpreting it on every execution. 

This is why you have a lot of power to influence the ultimate performance of your PHP code. 

Conclusion

I hope it was an interesting topic. As I mentioned at the beginning of the article I’m getting a lot of benefits from using this tactic and, in fact, they are also used in our packages.

You can see an example here of how I used these tips in our PHP package to optimize its performance.

If you want to learn more about the challenges of building a developers-driven company, you can follow me on LinkedIn.

 

 

 

 

Top