Skip to content

Benchmark: reflection vs a serialized injector vs CompiledInjector on a large real-world app #135

@koriym

Description

@koriym

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).

  • reflectionRay\Di\Injector (builds the Container from the module every process)
  • serialize — a serialize()d injector, unserialize()d per process
  • compiledRay\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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions