PHP-DI 6: turning into a compiled container for maximum performances

PHP-DI's motto is "The dependency injection container for humans".

This is very important because it defines what PHP-DI tries to be: practical to use, consistent, predictable, with a configuration format that is easy to read and write and a great documentation.

It also defines what PHP-DI does not try to be, and that is as much important. PHP-DI does not try to be the smallest, or the fastest container out there. And PHP-DI 5 is by far not one of the fastest containers.

But the good thing is that, after 6 years of existence, the project has matured and is now quite stable. The original objectives are met, even though there is of course always room for improvements and innovation. There is room to push the container to be better on other levels. And the most obvious one is performances.

PHP-DI 6 will be much, much faster because it is a compiled container.

What does "compiling" mean?

Imagine the following class:

class Foo
{
    public function __construct(Bar $bar) { … }
}

And the following configuration file:

return [

    Foo::class => create()
        ->constructor(get(Bar::class)),

    // Let's use a closure just for the sake of the example
    Bar::class => function() {
        return new Bar('Hello world');
    },

];

If we were to implement this without any containers, here is how we would instantiate Foo and Bar:

$bar = new Bar('Hello world');
$foo = new Foo($bar);

In a container, things go differently because the container is generic: it can handle 0 or many parameters, with references to a lot of entries, etc. To do that, the container constructs definition objects. A definition explains how to create an instance. More concretely, it is a class that stores the number of parameters of the constructor, which parameters need to be injected, etc.

If you are curious, you can read PHP-DI's ObjectCreator class: it takes a definition object and creates an object.

This process involves using PHP's Reflection API and a lot of logic (especially if the container is quite smart). And of course, this takes time. This is actually where PHP-DI spends most of its time. This is also the reason why "simple" containers like Pimple are faster than more complex ones: they have very simple (and limited) logic for creating objects.

Compiling the container means bypassing all that. The idea is to take all the definitions and turn them into optimized PHP code, close to the code we would write ourselves (except we don't have to write it ourselves).

Here is an theoretical example of a compiled container (not actual PHP-DI generated code):

// The class has a random number in there because it's auto-generated
class CompiledContainer123456 extends BaseCompiledContainer
{
    public function get($id)
    {
        // Method implemented in the parent class.
        // Basically it calls `$this->create{$id}()`
    }

    protected function createFoo()
    {
        return new Foo($this->get('bar'));
    }

    protected function createBar()
    {
        return new Bar('Hello world');
    }
}

Compiling the container means generating that class. Instead of manipulating definitions, the compiled container simply calls code that is optimized for your objects and your project.

Compiling closures

PHP-DI is not the first compiled container. Symfony's approach was a huge inspiration on how to tackle this problem.

However one thing specific to PHP-DI 6 is that even closures are compiled. For example:

return [
    Foo::class => function() {
        return new Foo();
    },
];

The Foo entry will be compiled and will benefit from the performance improvements. This will work even if the closure takes parameters, uses type-hinting, etc. This is made possible thanks to the awesome SuperClosure and PHP-Parser projects. The only other container that I know of that can compile closures is Yaco.

Usage

Compiling the container is very simple, you simply have to call enableCompilation():

$containerBuilder = new \DI\ContainerBuilder();

$containerBuilder->addDefinitions([
    Foo::class => create()
]);

$containerBuilder->enableCompilation(__DIR__ . '/var/cache');
$container = $containerBuilder->build();

The first time this code is run, the container will be compiled into a PHP file in the var/cache directory. On all future executions the compiled container will be used directly.

Integrating that with your deployment script should be easy too: simply clear the directory on every deploy and PHP-DI will recompile the container. You can also pre-generate the container in advance in your deploy script, just run the code above once.

Performances

Externals.io

Here is a comparison running PHP-DI 6 on externals.io (measuring the load time of the home page):

  • with cache enabled: 37ms
  • with cache and compilation enabled: 28ms

That means a 23% improvement in the total loading time, and 6% less memory used.

(Blackfire comparison)

Of course in a more complex application the time spent in the container will be lower so the gains will probably be lower.

On a side note, we can also compare v6 with v5 (Blackfire profile):

  • PHP-DI 5 with cache: 42ms
  • PHP-DI 6 with cache: 37ms

Even without compilation, externals.io runs 12% faster on v6 than on v5. This is thanks to many optimizations added during the development of version 6.

In total, with compilation enabled, migrating from PHP-DI 5 to PHP-DI 6 gives a 32% performance improvement on externals.io.

DI container benchmark

The gains shown above can also be seen in the kocsismate di-container-benchmarks. Here is a comparison of the results for PHP-DI 5 and PHP-DI 6. The left column is v5, the right is v6, and higher in the list is better:

  • Test suite 1
  • Test suite 2
  • Test suite 5
  • Test suite 6

PHP-DI now ranks in the fastest containers. Needless to say I am very happy with the results, especially since it is one of the most feature-rich containers.

Conclusion

Compiling the container brings massive performance improvements for the container and finally makes PHP-DI one of the fastest DI containers out there.

Let's keep in mind however that DI containers usually represents a small part of most application's run time. While I'm very happy to report all these improvements, do not expect your application to run twice faster in production ;)

Want to try this out? PHP-DI 6.0 will be released in the next weeks, in the meantime give the latest beta a try: https://github.com/PHP-DI/PHP-DI/releases

Comments