benchmark/di_benchmark.php here uses a small fixture. To show how the three strategies scale, these are numbers from a large production BEAR.Sunday application (~600 compiled DI scripts). The application name is withheld.
Setup: PHP 8.3, OPcache on, Xdebug off. Root = the application root (AppInterface), built once per process (php-fpm shared-nothing — each request is a fresh process).
- reflection —
Ray\Di\Injector (builds the Container from the module every process)
- serialize — a
serialize()d injector, unserialize()d per process
- compiled —
Ray\Compiler\CompiledInjector
Per-process cost to acquire the application root (≈ one php-fpm request, cold):
| strategy |
cost |
breakdown |
| reflection |
~0.4–0.6 s |
Container build (annotation reading + binding analysis + AOP weaving) dominates |
| serialize |
~29 ms |
≈ unserialize() of the whole Container (~25 ms, mostly class autoload) + build (~4 ms); blob ~0.5 MB |
| compiled |
~5 ms |
lazy require of only the scripts the root touches (~30 of ~600); offline compile ~0.5 s |
Takeaways
- reflection rebuilds the entire Container every process — untenable for shared-nothing. This is the cost the other two exist to avoid.
- serialize's runtime cost is essentially
unserialize() of the full container, so it scales ~linearly with the binding set (and the blob can't live in shared OPcache — it is re-unserialize()d per process).
- compiled lazily loads only what a request needs (sub-linear), and its scripts can be preloaded into shared OPcache across workers.
In a warm worker, unserialize() drops to ~1–2 ms (classes already loaded), so per-request serialize and compiled both land in the low-millisecond range; the dramatic gap is the cold first request and the linear-vs-sub-linear scaling.
Caveats: indicative single-run figures on one machine (macOS); the numbers above are cold / first-build (the figure that matters for php-fpm per-request). Warm steady-state at this scale was not cleanly isolated (validating OPcache for the full script set was hard), so treat those as approximate. The repo's benchmark/di_benchmark.php reproduces a small, OPcache-validated version of this comparison.
benchmark/di_benchmark.phphere uses a small fixture. To show how the three strategies scale, these are numbers from a large production BEAR.Sunday application (~600 compiled DI scripts). The application name is withheld.Setup: PHP 8.3, OPcache on, Xdebug off. Root = the application root (
AppInterface), built once per process (php-fpm shared-nothing — each request is a fresh process).Ray\Di\Injector(builds the Container from the module every process)serialize()d injector,unserialize()d per processRay\Compiler\CompiledInjectorPer-process cost to acquire the application root (≈ one php-fpm request, cold):
unserialize()of the whole Container (~25 ms, mostly class autoload) + build (~4 ms); blob ~0.5 MBrequireof only the scripts the root touches (~30 of ~600); offline compile ~0.5 sTakeaways
unserialize()of the full container, so it scales ~linearly with the binding set (and the blob can't live in shared OPcache — it is re-unserialize()d per process).In a warm worker,
unserialize()drops to ~1–2 ms (classes already loaded), so per-request serialize and compiled both land in the low-millisecond range; the dramatic gap is the cold first request and the linear-vs-sub-linear scaling.Caveats: indicative single-run figures on one machine (macOS); the numbers above are cold / first-build (the figure that matters for php-fpm per-request). Warm steady-state at this scale was not cleanly isolated (validating OPcache for the full script set was hard), so treat those as approximate. The repo's
benchmark/di_benchmark.phpreproduces a small, OPcache-validated version of this comparison.