PHP Performance I: Everything You Need to Know About OpCode Caches

Alternative Presentations:

 

What are OpCode Caches?

OpCode Caches are a performance enhancing extension for PHP. They do this by injecting themselves into the execution life-cycle of PHP and caching the results of the compilation phase for later reuse. It is not uncommon to see a 3x performance increase just by enabling an OpCode cache.

When Should You Use an OpCode Cache

Given that OpCode Caches have almost no side-effects beyond extra memory usage (to store the cache), they should always be used in production environments. The main side-effect to be concerned about is that there is some overhead caused by the initial caching — this, along with loss of the cache (new server, restarting Apache/php-fpm, restarting the machine) can lead to what is known as a cache stampede — however this can be mitigated by priming the cache.

Which OpCode Cache

Throughout PHPs life time there have been a number of OpCode caches; one of the first was from Zend (which has had several names), however it was proprietary. Therefore, for the last few years the primary cache being used has been APC — Alternative PHP Cache. While APC is great, it lacks some features found in Zend’s offering, and additionally lacked maintainers to bring it up to speed for the latest PHP versions.

 

With PHP 5.5, Zend open sourced their cache offering, under the new name of Zend OpCache, and contributed it to the PHP project — it is now included with PHP itself, in addition to being available for older PHP versions going all the way back to PHP 5.2!

 

Zend OpCache appears to be more performant than APC, more fully featured, and more reliable. However, Zend OpCache does not contain the secondary feature offered by apc, a userland shared memory cache — to mitigate this issue, a new extension “apcu” has been released, which provides just the userland caching, and is 100% compatible with the original APC implementation.

 

With many people not yet using PHP 5.5, or willing to move away from what they know, APC is still seeing wide use (and is enabled on Engine Yard Cloud by default even), and is still a great option for an OpCode Cache.

 

Moving forward (5.5+), Zend OpCache is the recommended option; there are known issues with APC and PHP 5.5. This document covers both Zend OpCache and APC.

Installation

 

After installing any of the following extensions, you will need to restart PHP, either by restarting your Apache or php-fpm. Additionally, you will want to install the Engine Yard PHP Performance Tools.

Zend OpCache

For PHP 5.5 and above, Zend OpCache is compiled as a shared extension by default unless you specify --disable-all when configuring. To explicitly enable it, you must specify --enable-opcache.

 

For PHP 5.4 or earlier (>= 5.2), you can install Zend OpCache using PECL.

 

$ pecl install zendopcache-beta

 

The pecl command will try to update your configure php.ini automatically. The file that pecl will try to update can be found using the following command:

 

$ pecl config-get php_ini

 

It will simply add the new configuration lines to the top of the indicated file (if any). You may want to move them to a more appropriate location.

 

Once you have the extension compiled (either alongside PHP, or via PECL), you must then enable it. To do so, you will need to add the following to your PHP INI file.

Note: If you don’t know your full path, your php.ini should have the path specified as the extension_dir directive. Alternatively, if you installed via PECL, it would have output a line (very near the end of it’s output) that looks like the following: Installing '/usr/local/php5/lib/php/extensions/no-debug-non-zts-20100525/opcache.so'.

APCu — APC User Cache (optional)

If want to take advantage of APC’s user cache, you will also want to install APCu. APCu is available via PECL. APCu provides a full backwards compatible API to the shared memory userland cache provided by APC.APCu should not be installed alongside APC.

 

$ pecl install apcu-beta

 

The install will then ask two questions, you can accept the defaults for both of them.

 

As with Zend OpCache, the pecl install may have added the appropriate lines to your configuration file. Again, you may want to move this to a more appropriate location. That configuration simply loads the extension:

With this in your configuration, you can now use the “apc_*” userland cache functions.

APC — Alternative PHP Cache

To install APC, you will once again use pecl:

 

$ pecl install apc-beta

 

This install will again ask you several questions, you can accept the default for each of these.

 

Once it has compiled, you need to add the following to your PHP configuration:

Engine Yard PHP Performance Tools

Installation of the Engine Yard PHP Performance Tools is done with composer. To install add the following to your composer.json:

Then run:

$ composer.phar update engineyard/php-performance-tools

This will install the latest version of the Engine Yard PHP Performance Tools.

Usage

Both Zend OpCache and APC are transparent in their operation — when a PHP file is executed, the opcodes are cached for later re-use.

 

However, as we mentioned, it’s possible to end up in a situation called a cache stampede. This occurs any time you have high load and your cache is not available — because it has yet to be built, or has been cleared for some reason.

 

To help solve this issue, you can pre-populate your cache, this is known as priming the cache.

Priming the Cache (Zend OpCache)

 

Unfortunately, at this time there isn’t a great way to prime the Zend OpCache. While APC provides some methods to do this, Zend OpCache does not — however this has recently been added to Zend OpCache and will be available in the next PECL release (and included with PHP 5.6).

 

In the meantime, there are other ways to achieve similar goals:

 

  1. Send multiple web requests to the server, causing Zend OpCache to cache any PHP used to generate it.
    1. This will only work for servers that are not currently under load
    2. This is potentially a lot of requests and may take some time to cache the required code
  2. Use multiple document roots to retain as much cache as possible when deploying code updates.

HTTP Cache

 

One way to accomplish our first option, is to use our integration testing test suite (or a subset thereof) if we have one. Otherwise, we can write a simple spider to make those requests.

 

One more thing we can do to improve this process (assuming you follow PSR-0 or similar standards) is to write a script that will include all of your classes in one script — caching a large majority of your application.

 

We have provided a simple HTTP Cache Primer in our Engine Yard PHP Performance Tools Repository, which can be found here.

 

This simple script uses the pecl_http extension to perform multiple requests in parallel; in testing, it can perform over 100 requests in about 4 seconds on a MacBook Air.

 

Note: If you do not have pecl_http, the script will simply make the requests serially, which is obviously much slower.

 

To use it, create a config.php based on config.php-dist in the current directory (a good location might be our build directory), containing a list of URLs you wish to perform requests against, like so:

Then we simply run the cache-primer:

 

$ /path/to/vendor/bin/cache-primer

 

This will give output similar to the following:

Or, with a much larger set of URLs:

Note: An additional benefit of caching in this way is that it will also prime any HTTP caches, such as Varnish, or Squid.

Upcoming Changes

With the addition of opcode_compile_file() in the next release of Zend OpCache, you will soon be able to prime the cache identically to APC below — just use zend-primer instead of apc-primer. Alternatively, you can use opcache-primer which will prime whichever cache is currently enabled.

Priming the Cache (APC)

 

While you can use the HTTP Cache Primer as with Zend OpCache, APC provides us with a way to directly cache any file we wish — while we still have to make a request to the web server, we can make one single request that will cache the entire code base.

 

We can do this using the APC provided apc_compile_file() function, which will compile the given file and add it to the cache. This means we can compile any valid PHP file, classes, templates, etc.

 

Similarly to the HTTP Cache Primer, we have to create a config.php based on config.php-dist, only this time it contains a list of directories in which to compile files, as well as (optional) configuration for HTTP Basic authentication.

 

Note: Basic HTTP authentication provides a minimal amount of protection for this script — a better option is to disable this authentication and include the script within your systems administrative interface.

Once you have created this file, you can then either create a simple wrapper that includes the apc-primer.php script, or you can integrate it into any other administrative interface you already have.

 

An example wrapper might look like:

When you request this file, after authenticating, it will recursively iterate through the contents of the configured directories, find all files with a .php and .phtml extension; then filter out those that reside in tests directories and then add them to the cache.

 

When run this will output something like:

Cached 4841 files in 2.1302671432495 seconds

 

At a little less than half the time it took for our HTTP Cache Primer it’s important to understand that we were able to cache all PHP code in our application, not just what is immediately accessible via HTTP. This is a much more completely primed cached.

 

Additionally, this only took one HTTP request, leaving more resources for actual clients to use.

 

However, on the negative side we did not additionally prime our Varnish or other HTTP cache alongside our opcode cache.

Under the Hood

 

Regardless of which OpCache you use, the way they work is pretty much the same. They inject themselves into the execution life-cycle and when possible short-circuit much of the repetitive work. Zend OpCache will additionally perform a number of optimizations when creating the cache.

The Execution Life-cycle

 

PHP is a scripting language, which most people take to mean that it is not compiled. While this is true in the traditional sense in that we are not calling a gcc or javac; instead we are compiling every time the script is requested. In fact, the PHP and Java compilation life cycles are pretty similar, because they both compile to an intermediary instruction set (opcodes, or bytecodes) which are then run in a virtual machine (Zend VM or JVM).

 

PHPJavaLifeCycle.png

 

The parsing and compilation phases is slow. When we add an opcache, we short-circuit this process by storing the result of the parsing and compilation phases, leaving just the execution to run dynamically as always. In effect, we are closer to the Java life-cycle now; with the main differences being that we saved to shared memory instead of a file, and can automatically re-compile if changes occur to the script.

 

 PHPOpCodeLifeCycle.png

Tokens & OpCodes

 

Once PHP gets ahold of your code, it creates two representations of your code.  The first is tokens; this is a way to break down your code into consumable chunks for the engine to compile into it’s second representation, opcodes. The opcodes are the actual instructions for the Zend VM to execute.

The Worst Hello World Ever

 

Taking a simple code example, a vastly over-engineered hello world example, lets look at both tokens, and opcodes.

Tokenizing

 

The first part of the compilation process parses the code into tokens. These are passed to the compiler to create OpCodes.

 

Token Name

Value

T_OPEN_TAG

<?php

T_CLASS

class

T_WHITESPACE

 

T_STRING

  Greeting

T_WHITESPACE

 
 

  {

T_WHITESPACE

 

T_PUBLIC

    public

T_WHITESPACE

 

T_FUNCTION

      function

T_WHITESPACE

 

T_STRING

        sayHello

 

        (

T_VARIABLE

          $to

 

        )

T_WHITESPACE

 
 

      {

T_WHITESPACE

 

T_ECHO

        echo

T_WHITESPACE

 
 

          "

T_ENCAPSED_AND_WHITESPACE

            Hello

T_VARIABLE

            $to

 

          "

 

        ;

T_WHITESPACE

 
 

      }

T_WHITESPACE

 
 

}

T_WHITESPACE

 

T_VARIABLE

$greeter

T_WHITESPACE

 
 

  =

T_WHITESPACE

 

T_NEW

    new

T_WHITESPACE

 

T_STRING

      Greeting

 

      (

 

      )

 

      ;

T_WHITESPACE

 

T_VARIABLE

$greeter

T_OBJECT_OPERATOR

  ->

T_STRING

    sayHello

 

    (

T_CONSTANT_ENCAPSED_STRING

      "World"

 

    )

 

;

T_WHITESPACE

 

T_CLOSE_TAG

?>

 

As you can see, most discrete piece of the code is given a name, starting with T_ and then a descriptive name. This is where the infamous T_PAAMAYIM_NEKUDOTAYIM error comes from: it represents the double colon.

 

Some tokens do not have a T_ name, this is because they are a single character — it would obviously wasteful to then assign them a much larger name — T_DOUBLE_QUOTE or T_SEMICOLON don’t make much sense.

 

It is also interesting to see the difference interpolation of variables makes; take the two strings, both double quotes, "Hello $to" and "World", the first, to account for the interpolated variables is split into 4 unique opcodes:

 

 

 

          "

T_ENCAPSED_AND_WHITESPACE

            Hello

T_VARIABLE

            $to

 

          "

 

and the non-interpolated string is simply one:

 

T_CONSTANT_ENCAPSED_STRING

      "World"

 

With this view we can start to see how small choices we make start to affect performance in miniscule ways (and frankly, especially in this case, as with single Vs double quotes, it’s not worth caring about!)

 

You can see the script that was used to generate this list of tokens here.

OpCodes

 

Next, lets look at how this looks like once we have compiled to opcodes. This is what the OpCode caches store.

 

To get the opcodes, we run the script through VLD, the Vulcan Logic Dumper, a pecl extension for PHP. To install it simple run:

 

$ pecl install vld-beta

 

And make sure the following is added to your PHP config:

Once you’ve done this, you can dump the opcodes for any code using the following on the command line:

 

$ php -dvld.active=1 -dvld.execute=0 <file>

 

VLD dumps global code (main script), global functions, and then class functions. However we’re going to look at our class functions first so as follow the same flow as the code itself.

Understanding VLD Dumps

A VLD dump is typically multiple dumps, one for the main script, then one for each global function and class function. Each dump is identical in structure.

 

First is the header which lists (if applicable) the class, and the function; then it lists the filename. Next it lists the function name (again, and only if applicable).

Next, it lists the total number of opcodes in the dump:

And then it lists the compiled variables (see below for details):

This is particularly important to take notice of.

Finally, it actually lists the opcodes, one per line, under the heading row:

Each opcode has the following:

 

  • line: The line number in the source file
  • #: The opcode number
  • *: entry (left aligned) and exit points (right aligned), indicated by greater than symbols (>)
  • op: The opcode name
  • fetch: Details on global variable fetches (super globals, or the use of the global keyword)
  • ext: Extra data associated with the opcode, for example the opcode to which it should JMP
  • return: The location where return data from the operation is stored
  • operands: the operands used by the opcode (e.g. two variables to concat)

 

Note: Not all columns are applicable to all opcodes.

Variables

There are multiple types of variables within the Zend Engine. All variables use numeric identifiers.

  1. Variable prefixed with an exclamation point (!) are compiled variables (CVs) — these are pointers to userland variables
  2. Variables prefixed with a tilde (~) are temporary variables used for temporary storage (TMP_VARs) of in-process operations
  3. Variables prefixed with a dollar ($) are another type of temporary variables (VARs) which are tied to userland variables like CVs and therefore require things like refcounting.
  4. Variables prefixed with a colon (:) are  temporary variables used for the storage of the result of lookups in the class hashtable

Dumping Hello World

Reading this, we see that we’re in the function sayHello of the class Greeting, and that we have one compiled variable, $to, which is identified by !0.

 

Next, following the list of opcodes (ignoring the no-op), we can see that:

 

  • RECV: The function receives a value which is assigned to !0 (which represents $to)
  • ADD_STRING: Next we create a temporary variable identified by a ~, ~0 and assign the static string, 'Hello+', where the + represents a space
  • ADD_VAR: After this, we concat the contents our variable, !0 to our temporary variable, ~0
  • ECHO: Then we echo that temporary variable
  • RETURN: Finally we return nothing as the function ends

 

Next, lets look at the main script:

Here we see one compiled variable, $greeter, identified by !0. So lets walk through this, again we’ll ignore the no-op.

 

  • FETCH_CLASS: First we lookup the class, Greeter; we store this reference in :1
  • NEW: Then we instantiate an instance of the class (:1) and assign it to a VAR, $2, and
  • DO_FCALL_BY_NAME: call the constructor
  • ASSIGN: Next we assign the resulting object (in VAR $2) to our CV (!0)
  • INIT_METHOD_CALL: We start calling the sayHello method, and
  • SEND_VAL: pass in 'World' to the method, and
  • DO_FCALL_BY_NAME: Actually execute the method
  • RETURN: The script ends successfully, with an implicit return of 1.

 

Go Forth and Optimize!

 

While there is a temptation to use this information to start doing micro-optimizations: don’t.

 

Use the opcode cache. It will give you more performance boost than any micro-optimizations possibly can. Additionally, when using Zend OpCache, a lot of optimizations are done for you (e.g. switching $i++ to ++$i when the return value isn’t used).

 

Using an opcode cache should not be optional anymore, it will enable you to get more performance from your hardware with very little effort. If you’re not using one yet, why not?

 

Resources

 

Comments

Article is closed for comments.