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.