How does APC work
APC is just simple, you don’t need to worry about too much and just enable it in apc.ini/php.ini, until you got some performance issue. The company I am working for takes performance issue very seriously because we need to serve millions of requests every single day.
Gopal V, one of the maintainer of APC, wrote this fantastic blog to talk about autofilter. However, I have to say it’s a little bit hard to understand until I actually read the APC source code on svn.php.net. The discussion of APC below is based on stable version 3.0.19.
Here are something you may or may not know.
Zend engine needs 4 steps to run a PHP script
1. Read PHP code from file into memory
2. Lexing : convert to lexicons that can form syntax
3. Parsing and compiling : parsing lexicons into opcodes and validate language syntax
4. Executing: execute opcodes
APC mainly hijacks step 3. Instead of having Zend to do step 1,2,3 again and again, APC actually stores the opcodes into shared memory , then copies opcodes into execution process so Zend can actually execute the opcodes. Classes and functions tables are also stored into shared memory because that’s what zend_compile_file generated.
Here are the detailed steps of what APC really does
1. During module init time ( MINIT), APC uses mmap to map file into shared memory.
2. During request time, APC replaces zend_compile_file() with its own my_compile_file().
What does my_compile_file do ?
1. If APC couldn’t find file in the cache, APC will call zend_compile_file to compile file
2. Get returned opcodes and store them into shared memory which read/write from different Apache child processes. ( We are talking about Apache prefork MPM here.)
3. Also store classes and function tables in shared memory.
4. Copy opcodes into Apache child process memory so Zend can execute the code.
Looks perfect and damn simple until you have read Gopal’s blog. At the beginning of his blog, Gopal mentioned that “The PHP compiler does generate an instruction to include file, but since the engine never executed it, no error is thrown for the absense of a.php.”. You can use parsekit to actually generate opcodes of your PHP code. ( parsekit can be installed by using pecl install parsekit). Below are the opcodes generated by parsekit.
 => ZEND_INCLUDE_OR_EVAL T(0) ‘./parent.php’ 0x4
 => ZEND_FETCH_CLASS NULL UNUSED ‘ParentClass’
Here is one example Gopal showed in his blog
index.php –include–> child1.php –include_once–> parent.php
|–include–>child2.php –include_once–> parent.php
profile.php –include–> child2.php –include_once–> parent.php
As you can see, if you are requesting index.php. Apache forks one child process for you. After that, Zend engine reads index.php from disk since it’s not in APC/memory, then parses and compiles it. At this time, APC gets opcodes and classes/functions tables and stores in shared cache. Until Zend executes index.php, it founds child1.php is required to finish execution and load/lexing/parsing/compiling child1.php again, then APC puts child1.php in shared memory. Again, Zend and APC do the same steps for parent.php.
As you may guess, Zend and APC will do the same thing for child2.php. Yes, you are right. But there is a tiny trick, when Zend compiles child2.php and it found that parent class in parent.php is already in process memory so it changed from ZEND_FETCH_CLASS to NOP which could save time to actually load parent.php again. Gopal called this version of opcodes *static* version and the version with ZEND_FETCH_CLASS is called *dynamic* version. This is exactly the same as static and shared/dynamic library.
Static is faster than dynamic, but it’s not always usable if parent is not in process yet. In this case, APC gives up storing this file’s opcodes in APC shared memory and forces Zend to compile again and again. This is how APC autofilter works. This is basically what Gopal told us in his blog, but why?
Isn’t parent.php already in APC cache for profile.php ? Why child2.php opcodes in APC cache couldn’t be usable ?
Here is why. The kicker is “smart” zend changing ZEND_FETCH_CLASS to NOP. When you try to access profile.php, apache most probably forks/uses preforked new process for you. Therefore Zend in new process has NO idea about index/child1/child2/parent.php. However, APC knew them already and had all the opcodes in its shared memory. When you request profile.php, Zend engine load/lexing/parsing/compiling profile.phph, then it notices that child2.php is in include path and APC knows child2 is already in shared memory cache. Before copying child2 cached version from shared memory to process memory, APC looks over all classes table in opcodes and restore aprent class pointer for compile-time inheritance. Boom!!! In current process, class parent has not been loaded yet and APC then thinks this opcodes is non-usable. This is why static version couldn’t be used here. In the other hand, dynamic version can always be used , but it’s slower.
Autoload has same issue, here I just copy comment in APC source code
* __autoload brings in the old issues with mixed inheritance.
* When a statically inherited class triggers autoload, it runs
* afoul of a potential require_once “parent.php” in the previous
* line, which when executed provides the parent class, but right
* now goes and hits __autoload which could fail.
* missing parent == re-compile.
* whether __autoload is enabled or not, because __autoload errors
* cause php to die.
* Aside: Do NOT pass *strlen(cl.parent_name)+1* because
* zend_lookup_class_ex does it internally anyway!
Simply to say, if you are using autoload, you have to pay penalty that you files cannot be in APC cache and Zend needs to recompile them again and again.
I hope I make myself clear.