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
intest1.php
). This is termed 'dynamic inheritance' since we have to dynamically (via some injected opcodes) perform inheritance.
- If the parent has not been loaded, the compiler generates the following opcodes (e.g.
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
NOP
s), the compiler generates the following opcodes (e.g.Child2
intest1.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
inphp.ini
, you will see the following when you loadtest1.php
followed bytest2.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!