DI Container Benchmark

Author:Máté Kocsis (@kocsismate90)
Repository:https://github.com/kocsismate/php-di-container-benchmarks
Generated:2021-06-19 11:05:54 UTC

Table of Contents


Introduction

In 2014, a really interesting benchmark about DI Containers for PHP was published on Sitepoint. Unfortunately, the implementation of the tests turned out to be quite controversial, so the benchmark itself wasn't very insightful.

I have been interested in the topic since then - and my curiosity was just growing after I had started to develop my own DI container, Zen - so I wanted to conduct another benchmark that also tries to measure real-life performance, while being as unbiased and reliable as possible. So here is my take! If you have any suggestion in mind about the benchmark or you want to add your container to the list, please create an issue or a pull request.

The examined containers are listed below along with some of their attributes:

Name Version Compiled/Dynamic Autowiring
aura/di 4.1.0 dynamic supported
chubbyphp/chubbyphp-container 2.0.0 dynamic not supported
level-2/dice 4.0.3 dynamic supported
joomla/di dev-2.0-dev dynamic supported
laminas/laminas-servicemanager 3.6.4 dynamic not supported
illuminate/container v8.47.0 dynamic supported
php-di/php-di 6.3.4 compiled supported
symfony/dependency-injection v5.3.2 compiled supported
yiisoft/yii2 2.0.42.1 dynamic supported
woohoolabs/zen 3.0.0 compiled supported

I'll try to give a vague definition below for some of the aforementioned notions:

A DI Container is compiled if it can be generated into a new class for production usage from where container entries then can be fetched. It means that your dependency graph is resolved during build time. This technique usually results in a very fast DIC, because there is no need for any Reflection or configuration when consuming the container. Dynamic containers however resolve your dependency graph Just-In-Time thus they are by design slower compared to the compiled ones.

A DI Container supports autowiring if it can be configured to automatically inspect and resolve at least some non-trivial subgraphs of the full dependency graph - no matter if the resolution takes place build time or run time. Otherwise all dependencies have to be resolved manually which is probably done as configuration. In this case, a DI Container does not support autowiring.

Essentially, dynamic containers usually need less attention during development than compiled ones, while containers which support autowiring usually need much less configuration than the ones without autowiring capabilities.


Method

Each container is given 6 tasks (Test Suites) where they have to create or fetch object graphs of different sizes (100, 1, or 1000 objects, respectively). For this purpose, containers are configured either to always instantiate objects (this is usually called as Prototype scope) or to instantiate objects only at the first retrieval and return the same instance on the subsequent calls (which is usually referred to as Singleton scope or shared services).

There are 3 main types of Test Cases: "Cold" ones measure performance including autoloading and bootstrap time of containers. "Warm" ones measure performance excluding autoloading and bootstrap time equally, while "Hot" ones additionally can warm up their caches before the measurements. The time of script compilation is always excluded from the results due to OPcache. Retrieved objects are always autoloaded (via preloading) prior to running the tests.

Each Test Suite contains three Test Cases which define how many times the main task has to be repeated in order to simulate different usage patterns. This number ranges from 10 to 100 000. Furthermore, all Test Cases are performed 30 times (this is referred to as "runs") in order to improve the accuracy of measurements. The median of these results is displayed in the final results.


Setup

The benchmark is run on AWS EC2, using a c5.large instance. The operating system on the host is Ubuntu 20.04. PHP 8.0 is running in a Docker container with OPcache enabled and autoloader optimized (using authoritative mode). Preloading is used for loading all classes besides the container-specific ones in order to keep the memory and autoloading overhead as small as possible. The code which performs the measurements is automatically generated in order to minimize the number of unnecessary instructions. During the measurements, a PHP-FPM script served by nginx is executed each time. This is needed because a production environment is simulated much better this way than in the CLI.

The examined DI Containers are configured for production usage as if it was probably done in case of a big project. That's why I took advantage of autowiring capabilities when possible.


Results

Test Suite 1: Instantiating an object graph of moderate size - Singleton scope

In this Test Suite, containers have to instantiate the same object graph of 100 objects (defined as Singletons) 1000 and 10 000 times. The first test case includes autoloading and bootstrap time of the containers in the measurements. The second test case excludes these. The third one warms up the container before the measurements.

1 000 iterations, container bootstrap time included (code)
Rank Container Time (ms) Time (%) Peak Memory (MB) Peak Memory (%)
1 Zen 0.09 100% 0.356 100%
2 Symfony 0.202 224% 0.947 266%
3 Dice 0.372 413% 0.742 208%
4 PHP-DI 0.374 416% 1.594 447%
5 ServiceManager 0.57 633% 1.39 390%
6 Chubbyphp 0.743 826% 1.353 380%
7 Yii2 Container 0.751 834% 1.784 501%
8 Laravel 1.155 1283% 1.62 455%
9 Joomla 1.317 1463% 1.635 459%
10 Aura 1.638 1820% 1.386 389%

1 000 iterations, container bootstrap time excluded (code)
Rank Container Time (ms) Time (%) Peak Memory (MB) Peak Memory (%)
1 Zen 0.076 100% 0.356 100%
2 Symfony 0.077 101% 0.947 266%
3 Chubbyphp 0.117 154% 1.353 380%
4 PHP-DI 0.148 195% 1.594 447%
5 ServiceManager 0.156 205% 1.389 390%
6 Joomla 0.192 253% 1.635 459%
7 Yii2 Container 0.3 395% 1.784 501%
8 Dice 0.355 467% 0.742 208%
9 Laravel 0.562 739% 1.62 455%
10 Aura 0.653 859% 1.386 389%

100 000 iterations, container already warmed up (code)
Rank Container Time (ms) Time (%) Peak Memory (MB) Peak Memory (%)
1 Zen 3.832 100% 1.868 100%
2 Symfony 4.015 105% 2.459 132%
3 ServiceManager 4.776 125% 2.901 155%
4 Chubbyphp 4.867 127% 2.865 153%
5 PHP-DI 5.121 134% 3.107 166%
6 Aura 5.337 139% 2.898 155%
7 Yii2 Container 5.367 140% 3.295 176%
8 Dice 6.675 174% 2.365 127%
9 Joomla 11.71 306% 3.146 168%
10 Laravel 26.831 700% 3.131 168%

Test Suite 2: Instantiating an object graph of moderate size - Prototype scope

In this Test Suite, containers have to instantiate the same object graph of 100 objects (defined as Prototypes) 1000 and 10 000 times. The first test case includes autoloading and bootstrap time of the containers in the measurements. The second test case excludes these. The third one warms up the container before the measurements.

100 iterations, container bootstrap time included (code)
Rank Container Time (ms) Time (%) Peak Memory (MB) Peak Memory (%)
1 Zen 0.543 100% 0.342 100%
2 Symfony 0.71 131% 0.933 273%
3 Chubbyphp 2.083 384% 1.339 391%
4 Joomla 3.157 581% 1.621 473%
5 ServiceManager 3.17 584% 1.376 402%
6 Dice 4.325 797% 0.607 177%
7 Yii2 Container 8.124 1496% 1.613 471%
8 Laravel 19.78 3643% 1.613 471%
9 Aura N/A N/A N/A N/A
10 PHP-DI N/A N/A N/A N/A

100 iterations, container bootstrap time excluded (code)
Rank Container Time (ms) Time (%) Peak Memory (MB) Peak Memory (%)
1 Zen 0.525 100% 0.342 100%
2 Symfony 0.537 102% 0.933 273%
3 Chubbyphp 1.486 283% 1.339 391%
4 Joomla 2.07 394% 1.621 473%
5 ServiceManager 2.753 524% 1.376 402%
6 Dice 4.31 821% 0.607 177%
7 Yii2 Container 7.683 1463% 1.613 471%
8 Laravel 19.175 3652% 1.613 471%
9 Aura N/A N/A N/A N/A
10 PHP-DI N/A N/A N/A N/A

100 iterations, container already warmed up (code)
Rank Container Time (ms) Time (%) Peak Memory (MB) Peak Memory (%)
1 Zen 0.495 100% 0.342 100%
2 Symfony 0.506 102% 0.933 273%
3 Chubbyphp 1.444 292% 1.339 391%
4 Joomla 2.017 407% 1.621 473%
5 ServiceManager 2.672 540% 1.376 402%
6 Dice 4.132 835% 0.678 198%
7 Yii2 Container 7.532 1522% 1.649 482%
8 Laravel 19.095 3858% 1.613 471%
9 Aura N/A N/A N/A N/A
10 PHP-DI N/A N/A N/A N/A

Test Suite 3: Instantiating a large number of small object graphs, Singleton scope

In this Test Suite, containers have to instantiate 1000 different objects (defined as Singletons) 100 times. The first test case includes autoloading and bootstrap time of the containers in the measurements. The second test case excludes these. The third one warms up the container before the measurements.

100 iterations, container bootstrap time included (code)
Rank Container Time (ms) Time (%) Peak Memory (MB) Peak Memory (%)
1 Zen 4.413 100% 2.054 100%
2 Symfony 5.094 115% 2.61 127%
3 ServiceManager 6.019 136% 3.013 147%
4 Chubbyphp 6.15 139% 2.994 146%
5 Yii2 Container 6.916 157% 3.484 170%
6 PHP-DI 7.897 179% 3.564 174%
7 Dice 8.624 195% 3.925 191%
8 Aura 10.52 238% 3.162 154%
9 Joomla 13.468 305% 3.258 159%
10 Laravel 29.194 662% 3.276 160%

100 iterations, container bootstrap time excluded (code)
Rank Container Time (ms) Time (%) Peak Memory (MB) Peak Memory (%)
1 Zen 4.396 100% 2.054 100%
2 Symfony 4.947 113% 2.61 127%
3 Chubbyphp 5.501 125% 2.994 146%
4 ServiceManager 5.653 129% 3.013 147%
5 Yii2 Container 6.44 146% 3.484 170%
6 PHP-DI 7.658 174% 3.564 174%
7 Dice 8.598 196% 3.925 191%
8 Aura 9.477 216% 3.162 154%
9 Joomla 12.442 283% 3.258 159%
10 Laravel 28.527 649% 3.276 160%

100 iterations, container already warmed up (code)
Rank Container Time (ms) Time (%) Peak Memory (MB) Peak Memory (%)
1 Zen 4.215 100% 2.076 100%
2 Symfony 4.393 104% 2.633 127%
3 PHP-DI 5.246 124% 3.587 173%
4 ServiceManager 5.246 124% 3.028 146%
5 Chubbyphp 5.29 126% 3.028 146%
6 Yii2 Container 5.545 132% 3.877 187%
7 Aura 5.84 139% 3.555 171%
8 Dice 7.269 172% 4.307 207%
9 Joomla 12.164 289% 3.277 158%
10 Laravel 27.238 646% 3.299 159%

Test Suite 4: Instantiating a large number of small object graphs - Prototype scope

In this Test Suite, containers have to instantiate 1000 different objects (defined as Singletons) 10 times. The first test case includes autoloading and bootstrap time of the containers in the measurements. The second test case excludes these. The third one warms up the container before the measurements.

10 iterations, container bootstrap time included (code)
Rank Container Time (ms) Time (%) Peak Memory (MB) Peak Memory (%)
1 Zen 0.916 100% 0.611 100%
2 Chubbyphp 2.024 221% 1.605 263%
3 Symfony 2.523 275% 1.52 249%
4 ServiceManager 3.171 346% 1.642 269%
5 Joomla 3.181 347% 1.887 309%
6 Dice 3.192 348% 1.437 235%
7 Yii2 Container 6.956 759% 1.91 313%
8 Laravel 14.527 1586% 1.839 301%
9 Aura N/A N/A N/A N/A
10 PHP-DI N/A N/A N/A N/A

10 iterations, container bootstrap time excluded (code)
Rank Container Time (ms) Time (%) Peak Memory (MB) Peak Memory (%)
1 Zen 0.905 100% 0.612 100%
2 Chubbyphp 1.468 162% 1.605 262%
3 Joomla 2.073 229% 1.887 308%
4 Symfony 2.362 261% 1.52 248%
5 ServiceManager 2.74 303% 1.642 268%
6 Dice 3.2 354% 1.437 235%
7 Yii2 Container 6.549 724% 1.91 312%
8 Laravel 13.972 1544% 1.839 300%
9 Aura N/A N/A N/A N/A
10 PHP-DI N/A N/A N/A N/A

10 iterations, container already warmed up (code)
Rank Container Time (ms) Time (%) Peak Memory (MB) Peak Memory (%)
1 Zen 0.833 100% 0.624 100%
2 Chubbyphp 1.357 163% 1.617 259%
3 Symfony 1.674 201% 1.54 247%
4 Joomla 1.986 238% 1.898 304%
5 Dice 2.557 307% 1.815 291%
6 ServiceManager 2.681 322% 1.653 265%
7 Yii2 Container 6.329 760% 2.299 368%
8 Laravel 13.87 1665% 1.851 297%
9 Aura N/A N/A N/A N/A
10 PHP-DI N/A N/A N/A N/A

Test Suite 5: Instantiating a single, large sized object graph - Singleton scope

In this Test Suite, containers have to instantiate the same object graph of 1000 objects (defined as Singletons) 100 and 10 000 times. The first test case includes autoloading and bootstrap time of the containers in the measurements. The second test case excludes these. The third one warms up the container before the measurements.

100 iterations, container bootstrap time included (code)
Rank Container Time (ms) Time (%) Peak Memory (MB) Peak Memory (%)
1 Zen 0.324 100% 0.377 100%
2 Symfony 0.442 136% 0.933 248%
3 PHP-DI 1.006 310% 2.137 567%
4 ServiceManager 1.164 359% 1.777 472%
5 Chubbyphp 1.233 381% 1.598 424%
6 Joomla 1.784 551% 1.855 492%
7 Yii2 Container 2.634 813% 3.709 985%
8 Dice 3.018 931% 4.975 1321%
9 Laravel 3.172 979% 3.03 805%
10 Aura 6.873 2121% 4.242 1126%

100 iterations, container bootstrap time excluded (code)
Rank Container Time (ms) Time (%) Peak Memory (MB) Peak Memory (%)
1 Symfony 0.308 100% 0.933 100%
2 Zen 0.309 100% 0.377 40%
3 Chubbyphp 0.613 199% 1.598 171%
4 Joomla 0.662 215% 1.855 199%
5 PHP-DI 0.747 243% 2.137 229%
6 ServiceManager 0.777 252% 1.777 190%
7 Yii2 Container 2.23 724% 3.709 397%
8 Laravel 2.588 840% 3.03 325%
9 Dice 2.997 973% 4.975 533%
10 Aura 5.872 1906% 4.242 455%

10 000 iterations, container already warmed up (code)
Rank Container Time (ms) Time (%) Peak Memory (MB) Peak Memory (%)
1 Zen 0.389 100% 0.531 100%
2 Symfony 0.407 105% 1.088 205%
3 ServiceManager 0.487 125% 1.931 364%
4 Chubbyphp 0.491 126% 1.753 330%
5 PHP-DI 0.507 130% 2.291 431%
6 Aura 0.537 138% 4.397 828%
7 Yii2 Container 0.539 139% 3.864 727%
8 Dice 0.728 187% 5.462 1028%
9 Joomla 1.174 302% 2.009 378%
10 Laravel 2.714 698% 3.185 600%

Test Suite 6: Instantiating a single, large sized object graph - Prototype scope

In this Test Suite, containers have to fetch an object graph of 100 objects (defined as Singletons) 1000, 10 000 and 100 000 times. Neither autoloading time, nor bootstrap time is included in the measurements.

10 iterations, container bootstrap time included (code)
Rank Container Time (ms) Time (%) Peak Memory (MB) Peak Memory (%)
1 Zen 0.832 100% 0.375 100%
2 Symfony 1.011 122% 0.94 251%
3 Chubbyphp 2.889 347% 1.566 417%
4 ServiceManager 3.909 470% 1.756 468%
5 Joomla 3.992 480% 1.853 494%
6 Dice 7.036 846% 3.553 947%
7 Yii2 Container 9.973 1199% 3.551 947%
8 Laravel 20.985 2522% 3.067 818%
9 Aura N/A N/A N/A N/A
10 PHP-DI N/A N/A N/A N/A

10 iterations, container bootstrap time excluded (code)
Rank Container Time (ms) Time (%) Peak Memory (MB) Peak Memory (%)
1 Zen 0.799 100% 0.375 100%
2 Symfony 0.837 105% 0.94 251%
3 Chubbyphp 2.291 287% 1.566 417%
4 Joomla 2.927 366% 1.853 494%
5 ServiceManager 3.521 441% 1.756 468%
6 Dice 7.026 879% 3.553 947%
7 Yii2 Container 9.502 1189% 3.551 947%
8 Laravel 20.363 2549% 3.067 818%
9 Aura N/A N/A N/A N/A
10 PHP-DI N/A N/A N/A N/A

100 iterations, container already warmed up (code)
Rank Container Time (ms) Time (%) Peak Memory (MB) Peak Memory (%)
1 Zen 5.485 100% 0.377 100%
2 Symfony 5.535 101% 0.941 250%
3 Chubbyphp 18.877 344% 1.567 416%
4 Joomla 25.019 456% 1.855 492%
5 ServiceManager 30.969 565% 1.757 467%
6 Dice 51.71 943% 4.272 1134%
7 Yii2 Container 81.801 1491% 3.911 1039%
8 Laravel 198.607 3621% 3.069 815%
9 Aura N/A N/A N/A N/A
10 PHP-DI N/A N/A N/A N/A


Conclusion

Keep in mind that in a well-architected application you won't call your DI Container hundreds or even thousands of times because ideally there should be as few composition roots as possible (but there is a good chance of needing the container in other places of the application layer - e.g. in your middleware or bootstrap files). That's why most results are exaggerated - you probably won't see milliseconds of difference between the fastest and the slowest DIC in the real life.

To sum up, when choosing a container it only depends on your needs which one suits your project best: if you have a performance-critical application then you probably want to choose a compiled container. If maximum performance is not required, but you develop a big and complex system then I would recommend a dynamic container with autowiring capabilities. Otherwise you can go with simpler containers.