Saturday, January 26, 2013

Composer: Speeding up class autoloading

The Use Case

Those of you using Composer have probably tried speeding up the PHP autoloading mechanism using the option:
--optimize-autoloader (-o): Convert PSR-0 autoloading to classmap to get a faster autoloader. This is recommended especially for production, but can take a bit of time to run so it is currently not done by default.

The Goal

While it can certainly make your autoloading faster it can be further optimized if you are using PSR-0-like autoloading mechanism and have quite a lot of classes.

The Problem

The problem with the classmap strategy and the nature of PHP is that there is no (easy) way to have a persistent variable across requests containing the classmap. Even if you are using APC, a PHP file returning a big array will always take some CPU cycles and memory because the array still needs to be recreated from the cached opcodes. This can even take a big portion of your request's response time when you have hundreds or thousands of classes like it is the case with eZ Publish 5 being based on Symfony, where about 2 600 classes are involved.

The Solution

I came across a quite simple solution that can surely be automatized from inside Composer itself.
Given that most dependencies of eZ Publish 5 are using the PSR-0 standard, I created a vendor/PSR-0 directory containing symbolic links to all relevant elements in such a way that there is a direct match between a namespaced class and the file system from a unique root directory:

$ tree vendor/PSR-0/
vendor/PSR-0/
├── Assetic -> ../kriswallsmith/assetic/src/Assetic/
├── eZ -> ../ezsystems/ezpublish/eZ/
├── EzSystems -> ../../src/EzSystems/
├── Monolog -> ../monolog/monolog/src/Monolog/
├── Psr -> ../psr/log/Psr
├── Sensio -> ../sensio/generator-bundle/Sensio/
├── Symfony
│   ├── Bridge -> ../../symfony/symfony/src/Symfony/Bridge/
│   ├── Bundle
│   │   ├── AsseticBundle -> ../../../symfony/assetic-bundle/Symfony/Bundle/AsseticBundle/
│   │   ├── FrameworkBundle -> ../../../symfony/symfony/src/Symfony/Bundle/FrameworkBundle/
│   │   ├── MonologBundle -> ../../../symfony/monolog-bundle/Symfony/Bundle/MonologBundle/
│   │   ├── SecurityBundle -> ../../../symfony/symfony/src/Symfony/Bundle/SecurityBundle/
│   │   ├── SwiftmailerBundle -> ../../../symfony/swiftmailer-bundle/Symfony/Bundle/SwiftmailerBundle/
│   │   ├── TwigBundle -> ../../../symfony/symfony/src/Symfony/Bundle/TwigBundle/
│   │   └── WebProfilerBundle -> ../../../symfony/symfony/src/Symfony/Bundle/WebProfilerBundle/
│   ├── Cmf -> ../../../vendor/symfony-cmf/routing/Symfony/Cmf/
│   └── Component -> ../../symfony/symfony/src/Symfony/Component/
└── Twig -> ../twig/twig/lib/Twig/

Once this has been setup, instead of using the usual Composer's autoload.php file, a custom autoloader is created for loading classes directly from that tree:

spl_autoload_register(
    function ( $className )
    {
        if (
            strpos( $className, "Symfony\\" ) === 0 ||
            strpos( $className, "Sensio\\" ) === 0 ||
            strpos( $className, "eZ\\" ) === 0 ||
            strpos( $className, "EzSystems\\" ) === 0 ||
            strpos( $className, "Monolog\\" ) === 0 ||
            strpos( $className, "Psr\\" ) === 0 ||
            strpos( $className, "Twig_" ) === 0 ||
            strpos( $className, "Assetic\\" ) === 0
        )
        {
            if (
                file_exists(
                    $path = __DIR__ . "/../vendor/PSR-0/" . str_replace( array( "_", "\\" ), "/", $className ) . ".php"
                )
            )
            {
                require $path;
            }
        }
        else
        {
            return;
        }
    },
    true
);

Conclusion

Once I did that change on a typical eZ Publish site configured with ezdemo package, my laptop was able to serve 227 reqs/second instead of the initial 176 reqs/second that is a 29% boost.

Action point

Composer team: any chance to be able to generate this automagically? :-)

13 comments:

lsmith said...

You cannot assume that you can just symlink directories, since in theory packages can place an individual file into overlapping subnamespaces, but I guess this just means that the symlink process needs to be smart enough to deal with that.

Patrick Allaert said...

@lsmith: I don't really see how this could lead to a problem. This is only used for autoloading purpose. The entire setup made by Composer is preserved.
Note that files not respecting the PSR-0 standard inside those directories can still be loaded by additional autoloaders by chaining them. It's perfectly valid to still use Composers' autoload after the one I propose.

Anonymous said...

Did you try using a preg_match instead of strpos to see if there is any further speed difference?

Patrick Allaert said...

@Anonymous: Nope, I haven't tried it. I started with strpos() since it is faster than preg_match(), but since there is several strpos(), it might be that a unique preg_match() is faster. I would say that it depends very much of the order you select. If using a serie of strpos() like I did, be sure to have the strpos() matching the most frequent used namespaces at the top and finish with the less frequently used at the bottom to minimize the number of strpos() called.

victor said...

The easy way exists, apc

Patrick Allaert said...

@victor Of course APC speed up things, but still if you use it (which is the first thing to do for any PHP application, performance wise) you will gain a lot with this technique in the case your classmap is big. Read carefully the post to see why, I'm actually mentioning APC in it.

victor said...

@Patrick: I was posting from my mobile, sorry for the lack of details.

I have indeed referring to the APC loader, based on the user cache (not to be confused with the opcode cache), see https://github.com/vicb/symfony/blob/master/src/Symfony/Component/ClassLoader/ApcClassLoader.php

You should also specify if you benchmarks have APC enabled or not to be complete.

Patrick Allaert said...

@victor: I'm fully aware of the ApcClassLoader since this is what we used in eZ Publish 5. The thing is that if you really have a big amount of classes (we have ~2600 classes), those methods are still not as fast as having a direct translation from namespace+classname to path+file that doesn't need any data fetching at all.
I'm not saying that this technique should always be used, but it's worth trying it when it comes to benchmark as it might make a difference on your project.

victor said...

@Patrick I am not saying that your proposal is bad either. However I would like to see a more exhaustive benchmark of your solution vs out of box class loading, composer optimized class loading and APC (user cache) optimized loading.

It seems a bit premature to me to ask the composer team to implement your solution w/o those performance data.

I think (stop me if I am wrong) that symlink would not work on windows. If the speed gain is limited, I would vote -1 for your solution.

IMO, it would really be great if you can detail your exact test environment and provide the additional perf data - I can't say what you are testing by reading your conclusion.

As a side note you might want to investigate generating your symlinks as a composer script.

Anonymous said...

1. Symlinks do work on windows, but when symlinking directories you have to be root

2. would it not just make sense to split a huge classmap file in many small chunks, one per included library? This way a project can include everything-and-the-kitchen-sink, but we could expect that most web pages would only load classes from a few libraries out of all the provided ones, thus reducing time spent and memory wasted

Stof said...

@Patrick did you tried to put the Composer classmap in the user cache instead of only relying on the opcode cache when recreating it ? I'm not sure it may help, but it would be worth a try.

Patrick Allaert said...

@Stof: nope, haven't tried that since I know the internals of PHP/APC a bit and know it wouldn't make any difference. The problem being that if you have an array containing thousands of entries, this array needs to be recreated in PHP userland space at every requests, regardless of where it comes from.

Patrick Allaert said...

For those who have asked, here is a detailed article containing the benchmarks: http://patrickallaert.blogspot.be/2013/03/benchmarking-composer-autoloading.html