Wayfair Tech Blog

PHP APC and Handling Dynamic Inheritance Opcodes

PHP APC and Handling Dynamic Inheritance Opcodes

Gopal Vijayaraghavan, one of the core APC developers, has a very good write-up ( APC Autofilter: The Real Story) on the consequences of static and dynamic inheritance on opcodes generated by the compiler as well as how APC handles them. If you're familiar with it, you might want to skip right to 'Handling Dynamic Opcodes' where we focus on some discrepancies we found as well as how to make things perform better.

We'll use these files to illustrate our points.

parent.php

<?php
class P {}
?>

child1.php

<?php
include_once 'parent.php';
class Child1 extends P {}
?>

child2.php

<?php
include_once 'parent.php';
class Child2 extends P {}
?>

test1.php

// Comments assume we're running test1.php followed by test2.php
<?php
include 'child1.php';
$c1 = new Child1(); // P will not be loaded when Child1 is loaded. (i.e. dynamic inheritance). Dynamic opcodes cached.

include 'child2.php';
$c2 = new Child2(); // P will be loaded when Child2 is loaded (i.e. static inheritance). Static opcodes cached.
?>

test2.php

// Comments assume we're running test1.php followed by test2.php
<?php
include 'child2.php';
$c1 = new Child2(); // P will not be loaded when Child2 is loaded (i.e. dynamic inheritance). Dynamic opcodes needed but static is in cache. Recompile.
?>

Static vs. Dynamic Inheritance

To refresh,

  • PHP tracks what classes it has loaded as it interprets the code.
  • When the definition of a subclass is encountered:
    • If the parent has not been loaded, the compiler generates the following opcodes (e.g. Child1 in test1.php). This is termed 'dynamic inheritance' since we have to dynamically (via some injected opcodes) perform inheritance.

Generated with Vulcan Logic Disassembler: http://pecl.php.net/package/vld

line # * op fetch ext return operands

2 0 > INCLUDEOREVAL 'parent.php', INCLUDEONCE
3 1 FETCHCLASS 4 :1 'P'
2 DECLAREINHERITEDCLASS $2 '%00child1%2Fwww%2Fchild1.php0xb73c6038', 'child1'
4 3 > RETURN 1

  • If the parent has been loaded, for efficiency (notice the NOPs), the compiler generates the following opcodes (e.g. Child2 in test1.php). This is termed 'static inheritance' since we don't have to do anything dynamic.

Generated with Vulcan Logic Disassembler: http://pecl.php.net/package/vld

line # * op fetch ext return operands

2 0 > INCLUDE OR EVAL 'parent.php', INCLUDE_ONCE
3 1 NOP
2 NOP
4 3 > RETURN 1

  • Because the static inheritance opcodes are only valid when the parent class is already loaded, it's not always safe to use (i.e. if the parent has not been loaded).
  • APC detects this and ensures that when dynamic inheritance is needed but the opcodes it has cached is static, it will perform a recompilation.
  • If you set apc.report_autofilter=true in php.ini, you will see the following when you load test1.php followed by test2.php:

Warning: include(): Dynamic inheritance detected for class child2 in /www/test2.php on line 2

Warning: include(): Autofiltering child2.php in /www/test2.php on line 2

Warning: include(): Recompiling /www/child2.php in /www/test2.php on line 2

The above is also true for autoloading since it's just a convenience for including files but does not change what the PHP compile has to do when generating opcodes for a class definition.

Handling Dynamic Opcodes

So far, everything has worked as detailed by Gopal.

However, Gopal goes on to say that the static opcodes will be evicted from the cache.

The blog post was written in 2007 so it's not surprising that things have changed. For APC 3.1.15, the behavior is different.

This is what APC does:

  • If the first time it encounters a class it is dynamic, it will cache that and serve it from the cache thereafter regardless of whether we encounter a static or dynamic class.
  • If the first time it encounters a class it is static, it will cache that.
    • Subsequently if it encounters the class and it can use the static opcodes, it will serve it from the cache.
    • However, if the class requires dynamic opcodes, it will ask PHP to recompile. It does not store these opcodes.

We can verify by running these steps:

  • load test1.php
    • child2.php is cached (Hits: 0).
  • load test2.php
    • Dynamic inheritance detected. Recompile.
    • child2.php is cached (Hits: 1). False hit. These are static opcodes and cannot be used.
  • load test1.php
    • child2.php is cached (Hits: 2). True hit, we're reusing the static opcodes.
  • load test2.php
    • Dynamic inheritance detected. Recompile.
    • child2.php is cached (Hits: 3). False hit. These are static opcodes and cannot be used.

Increasing Performance

While the above works, we can do better. Since we know that dynamic opcodes are always valid, once we've gone through the trouble of compiling it, we should store that in the cache and evict the static opcodes. Thereafter, we should serve those dynamic opcodes.

That is precisely what this patch ( opcodes generated by dynamic inheritance not cached) does.

After patching, you'll see the following (compare it to the previous run):

  • load test1.php
    • child2.php is cached (Hits: 0).
  • load test2.php
    • Dynamic inheritance detected. Recompile.
    • child2.php is cached (Hits: 0). Static opcodes have been replaced by the dynamic opcodes.
  • load test1.php
    • child2.php is cached (Hits: 1). True hit, dynamic opcodes are used.
  • load test2.php
    • child2.php is cached (Hits: 2). True hit, dynamic opcodes are used.

Here, we see that the static opcodes are cached until a dynamic inheritance is encountered. Then the dynamic opcodes are cached and served thereafter regardless of whether we encounter a static or dynamic inheritance.

Opcache: There's another opcode cache in PHP ( Opcache). Since we don't currently use this, we haven't yet determined how it handles the above scenario. If you've tried it out, we'd love to hear about it!

Share