Life Codecs @ NamingCrisis.net

Ruminations. Reflections. Refractions. Code.

Jul 29, 2009 - software dev

PHP & the APC Bytecode Cache

It is not often that I write about performance – okay more like never, and more so about PHP performance! But this is a must share methinks. On a personal project, I have been forced to use PHP for various reasons. Being rooted very much in the Java space, I went on a hunt for reusable stacks. I finally settled on:

  • Kohana for the MVC framework.
  • Doctrine for the ORM layer, since Kohana’s ORM annoyed me no end. Having worked with Hibernate in Java land, I was offended that Kohana called their implementation ORM – apologies folks, I am very biased here, the MVC bit, module system, cascading file layout, and hook system of Kohana is otherwise neat.

In the interest of ease of hosting and the existence of a vast knowledgebase from PHP’s standpoint, I settled on MySQL for the database. And of course, I use Linux, Debian in particular.

Anyway, Kohana tries to be extremely shared-hosting friendly, so it does not require having a long-running FastCGI process for example (not sure if there’s a recommended FCGI set-up), everything is loaded dynamically as per needed, no preloading of files in memory. Basically, every request, it loads up a bunch of PHP files, any startup hooks, routes the request to your controller, and your controller in turn loads up one or more classes to get its job done – DB queries, etc., you name it. For those of you in Java space, think of it as loading your servlet context at each request (yes, you heard me right) – somewhat of an exaggeration of course, but with enough stuff to load, it may very well be a suitable analogy, and in some ways worse — the source file is interpreted each time, no bytecodes cached by default (yet!). Even with this overhead, it is quite fast, very impressed.

But then I hooked in Doctrine using the integration module kindly provided here (had to upgrade the Doctrine version internally, but the Kohana hook points did not require changes, cool stuff). Now, Doctrine is a full-featured ORM, so it does have its overheads, it also features DQL – the Doctrine Query Language (inspired by HQL), which means for every DQL statement, it first parses it to the target SQL before execution. This caused each request to become a memory hog, from around 1-3 MiB per request prior to Doctrine usage (as output by the Kohana renderer, which in turn uses PHP’s builtin memory_get_usage function), it was now in the 8-12 MiB range per request – consistently!

The Doctrine documentation (v1.1 used here) has a rather decent section on performance, there were 3 main infrastructure-y (as opposed to application code) recommendations:

  1. Use a bytecode cache
  2. Use the doctrine query cache with an appropriate driver
  3. To minimise I/O due to multiple file inclusions, compile the Doctrine framework with a provided compile() function to get one large merged file encapsulating most (all?) of the Doctrine framework

Taking the above one at a time:

  1. There are a few bytecode caches around, but one that caught my virtual fancy was the Alternative PHP Cache (APC) – mainly because it seemed to be the easiest to install (on Debian, aptitude install php-apc) and most well-integrated – I feel that bytecode caching should be a default feature included with the runtime, and the buzz on the net seems to indicate that APC is more or less marked for inclusion by default into future PHP installs (but I could very well have misinterpreted the buzz :P). APC uses shared memory (shm) segments to cache PHP bytecodes, which while requiring some dedicated memory (duh), also makes it blazing fast. It does not, at least by default – have yet to explore – cache on disk (contrast this to Python .pyc files). I have not tuned any APC parameters, in my default install (which checks for source file changes to determine re-caching), it has literally brought the memory usage back down to 1-5 MiBs per request, with an average of 2.5 MiB, the higher end of the scale seems to occur when I load object graphs rather than associative arrays (another Doctrine recommendation, prefer array hydration over object hydration if you do not need the business logic on the objects). As my friend Tom would say, I am a happy camper! The app feels snappy!

    So yeah – PHP bytecode caches can make a HUGE difference! And the default settings for APC such as checking source file timestamps are perfect for development, I’ll have to check if tuning this setting makes any significant difference. A couple of downsides I can think of to the shm approach (again haven’t check tuning params whether APC does disks, etc.):

    • If you disable the source file checking, you’re potentially going to have to restart the process that allocated the shared memory if using a persistence process, I’ve only used FastCGI, not so sure how it affects mod_php with Apache
    • Also not sure how using dedicated shm for a process goes with shared hosting providers!

    • Prior to using the bytecode cache, I had enabled the Doctrine query cache, using the SQLite driver – so an SQLite database (direct file-access-based DB, no dedicated server process) is used as a cache, and this actually increased my memory usage and response times :P. Dang. Essentially what the query cache does is prevent the re-parsing from DQL to SQL each time, it uses the DQL as a key into the cache (or so I think that’s how it should work!) – effectively the query cache is a cache of prepared statements. However, Doctrine also comes with an APC driver for the query cache (another reason to use APC!), so once I had APC enabled, I replaced the query cache SQLite driver with the APC driver, not bad, it saves a further 0.3 to 0.5 MiBs per request!

    • Unfortunately, the compiled (merged to be more precise) Doctrine PHP file actually did not help me at all, it increased memory usage to about 7-8 MiBs after bytecode caching, before that it would’ve easily spiked to the 20 MiB range! Another thing I noticed was that I had to run compile() several times to create Doctrine.compiled.php (that was later included in lieu of just Doctrine.php) since it kept running out of memory. I had to increase the memory limit for a script from about 30MiB to around 100MiB for compile() to successfully complete and produce the merged file. Considering the number of files to merge, and it probably did this naively by loading all or most of them in memory and writing it out as a whole (a guess here), it is not surprising I suppose. The file produced is too large I think – in effect we killed lazyloading of classes by forcing a big read. And yes, I made sure I wasn’t reading BOTH doctrine.php and doctrine.compiled.php (it would not work anyway, we get class redeclaration errors!). Hmm, wonder if the recommendation was made for a set-up where the Doctrine init stuff was maintained in memory across requests.

To summarise:

  • The APC PHP bytecode cache kicks the proverbial @$$ so hard it hurts. Install it!
  • Doctrine’s query cache is neat, but so far only with APC – using SQLite may in fact be detrimental (it is after all yet another File I/O operation, opening a connection, etc. as opposed to an in-memory APC call).
  • The merged Doctrine PHP file actually made things worse!

Phew, hope that was useful!

Update Sat 2009-08-01:

Brain dump: I was thinking some more about the compiled Doctrine file… logically it should be faster because APC would put it into memory, and then simply just check one PHP file for timestamp updates to decide whether the cache should be invalidated for it. So why is it slower and used more memory; actually memory usage seems justified just not that much more? I haven’t confirmed but I wonder if the bytecodes could not fit into the cache… and therefore had to be read from the file each time… and since there’s no lazy loading (the whole file is interpreted at once — the whole Doctrine framework!), it’s hungrier? Hmm, will confirm when I am inclined to.