For an overview of autoloading in general, please see my previous article
Much fuss is made over the performance penalties of using PHP’s magic autoloading functionality. While I believe that it reduces overall project complexity and increases productivity, there are some very high-traffic projects I’ve overseen where the performance overhead becomes noticeable. This is particularly true for page requests that are already sensitive to delays, such as AJAX or REST requests. In this article, I’d like to showcase a couple of options for mitigating (and even eliminating) autoloader performance penalties.
Please note that all code examples in this article are strictly for illustrative purposes, and are not at all ready for production sites. These concepts should serve as starting points to implement your own high performance autoloaders
The Problem
While autoload is a godsend for managing large projects, you do incur a small performance penalty for its use. This overhead occurs primarily because:
- Most autoloader methods search through the include path
- Most autoloader methods include files using relative paths
- PHP must devote execution time to Just In Time load each class. This magic comes at a price.
If this process occurs a few dozen times per request, and requests are popping in around 10/second, you might find yourself in a boring meeting discussing how the hell your team can decrease latency.
Step 1: Include Patch Caching
By and large, the content of include directories for production projects does not change. If a class file was found during the last request, why search for it again during the current one?
Rather than simply calling include() (or require() ) from your autoloading methods, do a little digging to discover the absolute path of the file for a particular class. This will be an expensive operation, but it need only be performed once. Each subsequent request can simply result in the direct inclusion of the correct file, rather than a hunt through the include_path. An example:
-
class CodeFeast_Loader
-
{
-
{
-
//check if this class is cached
-
{
-
//It was cached. No need to search, just load
-
require(self::$file_location_cache[$class]);
-
}
-
//It wasn’t cached. We’ve got to hunt
-
else
-
{
-
//did we nab the include_path yet?
-
if(!$include_paths_loaded)
-
{
-
self::populateIncludePaths();
-
}
-
//Look in each path for the class file
-
foreach(self::$include_paths as $path)
-
{
-
{
-
//We found our file. Add it to the cache
-
self::$file_location_cache[$class] = $path . "$class.php";
-
//And load it
-
require(self::$file_location_cache[$class]);
-
}
-
}
-
}
-
}
-
//grab the include_path from php.ini
-
{
-
self::$include_paths_loaded = true;
-
}
-
//load the cache from ‘tmp’
-
{
-
$file_location_cache =
-
self::$file_location_cache =
-
-
}
-
//persist the cache to dir ‘tmp’
-
{
-
$file_location_cache =
-
file_put_contents(‘tmp/file_location_cache.bin’,
-
$file_location_cache);
-
-
}
-
//init the autoloader
-
{
-
//Register the autoloader
-
spl_autoload_register(
-
);
-
-
//Restore the cache
-
self::openCache();
-
}
-
}
-
-
CodeFeast_Loader::start();
-
-
//Code for your application goes here
-
-
//Call the saveCache() method to persist the cache across requests
-
CodeFeast_Loader::saveCache();
Using this autoloader class, the actual search for the file is only performed the first time a file is requested. Furthermore, performance is enhanced by the calling of require() rather than require_once(), and by calling it with the absolute path to the file requested.
Step 2: Anticipatory Autoloading
So, you’ve implemented path caching in your autoloaders, but your users are still complaining that their annoying popups aren’t loading fast enough? Let’s figure out how to keep the convenience, but completely eliminate calls to autoload.
Much like the include_path, the specific classes loaded for each section of your application are unlikely to change from request to request. If we can find a unique identifier that will partition our apps into segments (such as the URL), we can simply have our autoloader remember what files it had to load from the last request, and skip the autoloader overhead all together.
Let’s start with our previous example, and add some enhancements:
-
class CodeFeast_Loader
-
{
-
{
-
//check if this class is cached
-
{
-
//It was cached. No need to search, just load
-
require(self::$file_location_cache);
-
}
-
//It wasn’t cached. We’ve got to hunt
-
else
-
{
-
//did we nab the include_path yet?
-
if(!$include_paths_loaded)
-
{
-
self::populateIncludePaths();
-
}
-
//Look in each path for the class file
-
foreach(self::$include_paths as $path)
-
{
-
{
-
//We found our file. Add it to the cache
-
self::$file_location_cache[$class] = $path . "$class.php";
-
//And load it
-
require(self::$file_location_cache[$class]);
-
//And add it to the segment cache
-
self::$app_segment_cache[self::$app_segment] [] =
-
self::$file_location_cache[$class];
-
}
-
}
-
}
-
}
-
//grab the include_path from php.ini
-
{
-
self::$include_paths_loaded = true;
-
}
-
//load the cache from ‘tmp’
-
{
-
$file_location_cache =
-
self::$file_location_cache =
-
-
$app_segment_cache =
-
self::$app_segment_cache =
-
unserialze($app_segment_cache);
-
}
-
//persist the cache to dir ‘tmp’
-
{
-
$file_location_cache =
-
file_put_contents(‘tmp/file_location_cache.bin’,
-
$file_location_cache);
-
-
$app_segment_cache =
-
file_put_contents(‘tmp/app_segment_cache.bin’);
-
}
-
//init the autoloader
-
//We add a method to identify which part of the app we’re in
-
//This could be a URL, or a controller action in MVC
-
{
-
self::$app_segment = $app_segment;
-
//Register the autoloader
-
spl_autoload_register(
-
);
-
-
//Restore the cache
-
self::openCache();
-
-
//Load every cached autoload for this segment as a simple require
-
{
-
foreach(self::$app_segment_cache[$app_segment] as $include)
-
{
-
require($include);
-
}
-
}
-
}
-
}
-
-
//We’ll start the autoloader using the URL as the app segment
-
CodeFeast_Loader::start($_SERVER[‘REQUEST_URI’]);
-
-
//Code for your application goes here
-
-
//Call the saveCache() method to persist the cache across requests
-
CodeFeast_Loader::saveCache();
And now we’ve rid ourselves of autoload’s performance overhead without sacrificing any of its convenience.
This class has little effect on the initial, uncached request to an app segment, but really shines on subsequent requests.
Since this class remembers what it autoloaded, it can eliminate itself all together and bring what it will need in as simple includes. Best of all, it still has autoload functionality in the unlikely event that the required classes change between requests.
Since we’ve chosen to segment our app by URL, I’ll use “index.php” as an example. You could also easily segment by module, controller, or action.
When the first request is made for the location “index.php”:
- The autoloader is woken up and restores its cache files
- Every class that is needed for index.php will be autoloaded as usual
- The autoloader will remember everything it had to autoload for “index.php”
Now, when the second request is made for this same location, this happens:
- The autoloader is woken up and restores its cache files
- Every class that it had to autoload last time is immediately included
- The people rejoice
Security Note
Do not store the autoloader cache files in /tmp, or any other world writable area. The last thing any of us needs is some little weenus having the ability to arbitrarily load include files on our server. Also, you really should perform some sanity checks on the files you’re including in your autoloaders.
Conclusion
I hope these crude classes effectively illustrate that you needn’t suffer autoloader overhead with every request. I should also note that I don’t advocate the use of steroids. Sure, you look great on the beach, but they shrink your giblets.
