Boosting PHP performance: mastering OPcache optimization with Blackfire

Unlock the full potential of your PHP applications with our comprehensive guide to OPcache optimization. Dive deep into the PHP compilation mechanism and see how Blackfire’s continuous observability solution can ensure your settings are always optimized for the best performance.

By Thomas di Luccio, on Jul 17, 2024

We believe that embracing a performance optimization journey can be transformative. It’s about breaking the endless reactive hotfix cycle and replacing it with a more virtuous one. One where developers incrementally regain control over their applications’ performance until they can proactively identify existing bottlenecks or the consequences of upcoming changes before they reach production.

As for most experiences, the journey matters even more than the destination. We usually learn valuable lessons and develop new skills along the way. Observability is about providing the ability to witness what’s normally invisible.

By visualizing code execution and the overall application dynamics, Blackfire users put themselves in a situation to make educated decisions. We provide actionable insights that guide users, flag optimization opportunities, and educate them simultaneously.

Maya Angelou, the brilliant American poetess, advised us to, “do the best you can until you know better. Then, when you know better, do better.” While Maya Angelou’s words were expressed in an entirely different context, they still resonate with our work as developers. The more we know, the better we’ll do. And there is so much to know in our industry!

Dive into PHP compilation and caching mechanisms

One lesser-known fact in the PHP ecosystem is how code is compiled and how internal caching mechanisms can be optimized to take advantage of that processing. Let’s grab our flippers, masks, and snorkels; we’re diving deep into the PHP core itself!

When a PHP script is executed, the PHP engine first tokenizes the code, splitting it into an organized series of tokens. Then, those tokens are parsed into an Abstract Syntax Tree (AST).

The Abstract Syntax Tree was introduced in 2014 as part of PHP 7 as an intermediary structure in the compilation process. Before its implementation, the parser was also responsible for emitting opcodes. The PHP core team aimed to implement a more maintainable and understandable process by decoupling the parser and compiler.

You can explore AST with phpast.com, created by Ryan Chandler. This site lets you transform any PHP code into a syntax tree. It’s a great way to experiment with the normalization process happening behind the scenes while your code is executed.

What are opcodes?

We have seen that executing starts with tokenizing and parsing it into an abstract syntax tree. Then, the AST is compiled into opcodes, or operation codes, which are low-level instructions that the Zend Engine (the core of PHP) can execute. Finally, the generated opcodes are executed, producing the final output.

The OPcache extension, bundled with PHP since v5.5, provides some debug features allowing you to dump and explore the generated opcode by enabling opcache.opt_debug_level=1 in your .ini file.

Let’s consider a straightforward example with a test.php file containing a simple echo "Hello World!"; instruction. The abstract tree above describes the same instruction.

<?php

// test.php

echo 'Hello World!';

Then, running the following command returns the entire opcode for that simple script:
php -d opcache.opt_debug_level=1 -d opcache.enable_cli=1 test.php

$_main:
 	; (lines=2, args=0, vars=0, tmps=0)
 	; (after pass 1)
 	; /Users/thomas/code/opcode/test.php:1-3
 	; return  [] RANGE[0..0]
0000 ECHO string("Hello World!")
0001 RETURN int(1)


Hello World!

What’s OPcache?

OPcache is a built-in cache service that stores precompiled script bytecode in memory, thereby removing the need for PHP to load and parse them on each request. OPcache can, therefore, significantly impact your application’s performance since only the low-level code will be executed without the need for resource-intensive parsing and compilation. A bytecode is a chunk of opcode.

This means that, when a PHP script is requested, the PHP engine first checks if the OPcache extension is enabled. If so, OPcache checks if the script’s bytecode is already cached. If it is, the cached bytecode is fetched directly from shared memory, bypassing the lexical analysis, parsing, and compilation stages.

If the script’s bytecode is not cached, the PHP engine processes the script as usual (tokenizing, parsing, and compiling it into opcodes). The generated bytecode is then stored in shared memory for future requests. The fetched or newly generated bytecode is executed by the Zend Engine.

Making the best out of OPcache

Diving deep into PHP internals lets us grasp its behavior and highlight the high potential of its own caching mechanism. While it might actually just work for some projects, the best is to continue our journey to ensure we have enough control over OPcache and our PHP applications’ performance.

OPcache comes with a fairly long list of parameters. Let’s walk through some of them to get you started. PHP documentation lists and explains them all. You can check the value defined in your projects using phpinfo() or running php --info | grep opcache:

  • opcache.enable (boolean) enables the opcode cache. The default value is 1, and OPcache should be enabled by default for the HTTP traffic.
  • opcache.enable_cli (boolean) enables the opcode cache for PHP CLI. The default value is 0. It might be worth considering turning it on if your application uses critical or resource-intensive CLI commands.
  • opcache.memory_consumption (integer, default 128) is the size of the shared memory storage used by OPcache in megabytes. This value has to be high enough to store all bytecodes. A low value will prune parts of the cache and constantly recompile some code. Consider at least 128Mb.
  • opcache.max_accelerated_files (integer, default 10,000) refers to the maximum number of keys, therefore scripts, in the OPcache hash table. A low value will also lead to swap operations, with existing entries removed and new ones compiled.
  • opcache.interned_strings_buffer (integer) defines the amount of memory used to store bytecodes, in megabytes. The default value is 8 and you may consider slightly higher values to ensure smooth caching operations (opcache.interned_strings_buffer=12)

Then, we have to consider a larger caching strategy. How often do we want to reset and regenerate the opcode cache? How long should entries remain stored? Consider the following settings:

  • opcache.validate_timestamps (boolean, default true): If enabled, OPcache will check for updated scripts every opcache.revalidate_freq seconds. When this directive is disabled, OPcache has to be manually reset.
  • opcache.revalidate_freq defines how often script timestamps must be checked for updates, in seconds. 0 will result in OPcache checking for updates on every request. This directive is ignored if opcache.validate_timestamps is disabled.

Depending on how your application deployment process works, you might consider invalidating the OPcache revalidation process and eventually explicitly resetting it when deploying a new version of your application.With a Platform-as-a-Service (PaaS), such as Upsun, this could take shape with this config file:

applications:
  my-php-application:
    ...
    type: "php:8.3"
      variables:
        php:
         opcache.enable: 1
         opcache.enable_cli: 1
         opcache.memory_consumption: 256
         opcache.max_accelerated_files: 50000
         opcache.interned_strings_buffer: 12
         opcache.validate_timestamps: 0
         opcache.preload: /path/to/the/preload/file
         ...
    hooks:
      deploy: |
        ...
        php -r 'opcache_reset();'

Last but not least, the opcache.preload ini setting specifies a PHP script that will be compiled and executed at server start-up. All the entities defined in these files will be available to requests out of the box, until the server is shut down. Discover how Symfony, Laravel, and Drupal could use that critical feature.

Using Blackfire to track OPcache usage

All profiles have a Cache tab containing information on all internal caching systems, their configuration, and current usage. For OPcache, the memory consumption, interned string buffer, and the number of accelerated files are tracked and displayed.

Each of these three pieces of information is bound to the ini settings defined above. It’s critical to ensure OPcache is correctly configured and has enough room to operate swiftly.

Blackfire Monitoring also tracks internal cache usage over time. For OPCache, the OPcache hit rate, OPcache usage, and OPcache interned strings buffer Usage are tracked and displayed.

The OPCache hit rate represents the percentage of times a PHP script’s precompiled bytecode is successfully retrieved instead of having to be recompiled from the source code. This value has to stay as high as possible and close to 100%.

The OPcache usage, and OPcache interned strings are also related to their respective ini settings. Those values should never reach 100% as it would mean some cached opcodes were pruned and scripts recompiled.

You should now have a better understanding of PHP internals and its own code caching mechanism. With Blackfire, you should be in a position to control your application configuration better and make long-lasting optimization of its performance.

Here are some resources if you wish to go even further:

Let’s continue the conversation on Dev.to,  Slack, Reddit, or our new community portal. Let us know your OPcache configuration and strategy, and you are fine-tuning your application to go the extra mile in performance optimization.

To better observability and beyond!

Thomas di Luccio

Thomas is a Developer Relations Engineer at Platform.sh for Blackfire.io. He likes nothing more than understanding the users' needs and helping them find practical and empowering solutions. He’ll support you as a day-to-day user.