Patrick Kerrigan

PHP performance optimisation quick wins

by Patrick Kerrigan, . Tags: Php Web Performance

PHP apps can easily begin to slow down over time as they grow, and with the recent patches for the Meltdown vulnerability adding a performance penalty to many workloads this slowdown can be amplified. Often there are some simple changes that can be made in order to see a measurable performance improvement for production workloads.

Background

These tips come from experience with large, non-trivial codebases. I don't provide any benchmarks here as what works for one person won't necessarily work in the same way for others, however for me these changes have resulted in response times being more than halved.

If you're looking to find performance improvements that work well for your app, then XDebug and CacheGrind can be invaluable tools.

Use PHP 7+

The single biggest performance improvement you can get for a PHP 5 app is likely to be upgrading it to PHP 7. Doing so is by no means a trivial task and is worthy of a post of its own, but in my experience, even for large code bases, the time invested in making the switch is well worth it. PHP 7 runs up to twice as fast as PHP 5 with the exact difference being highly dependent on your workload.

Enable OPcache

OPcache is an extension that's been bundled with PHP since version 5.5, and is available as a PECL extension for versions 5.2 - 5.4. The purpose of OPcache is to cut out a large chunk of the time it typically takes to respond to a request with PHP by caching compiled bytecode in memory. When enabled, requests that would normally cause all required PHP files to be loaded from disk, parsed, compiled to bytecode then executed get to skip straight to the last step - bytecode is executed straight from memory. While this doesn't quite give the same level of performance as languages such as Java, the performance boost is substantial and there's no reason not to enable OPcache if you have control over your PHP installation.

There are lots of configuration options for OPcache that should be tuned to suit your particular app, however the following is a good starting point for a medium sized app in production:

opcache.enable = 1
opcache.memory_consumption = 128
opcache.interned_strings_buffer = 8
opcache.max_accelerated_files = 4000
opcache.revalidate_freq = 60
opcache.fast_shutdown = 1
opcache.enable_file_override = 1
opcache.enable_cli = 0

For a development environment, you'll want to change opcache.revalidate_freq to:

opcache.revalidate_freq = 2

A useful tool to help you tune OPcache to your particular use case is Rasmus Lerdorf's OPcache status page. Keep an eye on the memory usage and number of cached keys to give you an indication as to whether you need to change the memory consumption or max accelerated files values.

Use Composer's autoloader

Most modern apps already rely on the autoloader provided by Composer to load their classes. Older code bases however often have their own autoloader logic that may be adding a fairly significant performance hit.

A common way of autoloading classes is to take the name of the class, replace namespace separators (for PSR-4 classes) and underscores (for PSR-0 classes) with the directory separator, then check in one or more directories until you find the file that contains it. The problem with this is that it normally results in at least one (normally more) calls to file_exists for every class that's loaded, which can be in the thousands for more complex apps. Each call to file_exists results in a syscall (although OPCache can sometimes prevent this) which with the recent Meltdown patches can be relatively slow. Avoiding 4,000 syscalls per request is likely to give you a noticeable performance boost.

Composer can solve this for us. It has a feature which allows you to generate a static mapping of class names to file names, which is then compiled as PHP and stored in memory by OPcache as bytecode. With this in place, when your code needs to load a class, all of the information it needs is already present in memory, and no disk access needs to occur. Provided your code follows PSR-4 and/or PSR-0 this can be as simple as adding a few lines to your composer.json file. The following would allow for a mixture of PSR-4 and PSR-0 classes stored in the "src" directory to be loaded:

"autoload": {
    "psr-4": {"": "src/"},
    "psr-0": {"": "src/"}
}

All that's left is to include Composer's autoloader in place of your own by requiring the "vendor/autoload.php" at the start of your app's entry point, then run

composer install

By default, Composer will dynamically check the filesystem for classes which is what you normally want for a development environment. To enable the compiled class map discussed earlier, you should include the following in your production builds

composer install -o

Use lazy connections

Modern PHP apps connect to many different services ranging from databases to message queues. One common killer of performance is connecting to all of these services whether they're needed for the current request or not. Take message queues as an example; it can be relatively rare to need to publish a message to a queue, but some apps will establish a connection to the server before they even start to process the request. A network connection is an extremely expensive operation, so avoiding them where possible is an easy way to get a speed boost.

Most framework components that communicate with remote services will provide a configuration option to use "lazy" connections, to connect on demand or similar. If you're using one of these frameworks then you can just enable this option and see the benefits immediately.

For apps which have their own custom connections to remote services this can be accomplished by moving connection logic out of class constructors. Connecting in the constructor causes the connection to be established when your app is bootstrapped before the request (by a dependency injector or similar). Connecting at the start of any method which interacts with the remote service will ensure that this performance penalty is only incurred when it's guaranteed that it's needed.