50× faster hydration and 60× faster serialization than the field-leading PHP data-object library — without changing how you write DTOs.
Benchmarked against the most popular full-featured data-object library in the PHP/Laravel ecosystem — identical DTO shapes and attributes, inside a fully booted Laravel app with both libraries' caches warmed, 20,000 iterations per scenario after a 2,000-iteration warmup, PHP 8.4. Medians of 7 runs (5 for XML). Absolute numbers vary with hardware; the ratios stay stable across runs.
Throughput
higher is better · each scenario scaled to its own leaderStreaming a 100,000-row CSV import
higher is betterRows from a generator, hydrated one by one. Both libraries stream here and hold the same flat 12 KB — the difference is speed, not memory.
Streaming XML — 100,000 elements, 52 MB file
each scenario in its own processThe alternative has no XML support, so throughput is measured against what a consumer writes by hand: an XMLReader loop mapping each element to an array. That loop stays as flat on memory as lazyXml() — at a fifth of the speed. Memory is compared with SimpleXML twice: a careful loop that handles one element at a time and accumulates nothing, and the heaviest common pattern, collecting every row into an array before hydrating. The same app sitting idle is at 54 MB.
CPU time per operation follows the same ratios — less CPU burned per request means more headroom per server. The advantage is largest where the object itself is cheap and shrinks as real work (a date cast, twenty nested objects) takes a bigger share of each call. The from()/toArray() hot paths execute compiled per-class closures, and lazyCollection() keeps peak memory flat on any dataset size. lazyXml() does the same straight from a file: only the fields the DTO declares are read, so a 100,000-element document adds about 1 MB to the process. The SimpleXML figure is process memory (RSS), not the PHP heap — libxml builds the whole document outside PHP's memory manager, so memory_get_peak_usage() reports roughly 10 MB for lazyXml() and for the SimpleXML loop alike; only the collected variant shows up in the heap, at about 420 MB.
The numbers
| Scenario | Simple Data Objects | Popular alternative | Advantage |
|---|---|---|---|
| Hydration — flat DTO | ~6,300,000 ops/s | ~125,000 ops/s | ~51× |
| Hydration — nested DTO | ~3,400,000 ops/s | ~93,000 ops/s | ~36× |
| Hydration — collection of 20 | ~211,000 ops/s | ~10,200 ops/s | ~21× |
| Hydration — with a date cast | ~1,400,000 ops/s | ~118,000 ops/s | ~12× |
| Serialization — flat DTO | ~14,500,000 ops/s | ~241,000 ops/s | ~60× |
| Serialization — nested DTO | ~7,700,000 ops/s | ~162,000 ops/s | ~48× |
| Serialization — collection of 20 | ~415,000 ops/s | ~27,500 ops/s | ~15× |
| CSV — 100,000 rows, streamed | ~67,000 rows/s | ~36,000 rows/s | ~1.85× |
| CSV — peak memory while streaming | 12 KB | 12 KB | equal |
| XML — 100,000 elements, streamed | ~80,000 nodes/s | ~15,000 nodes/s | ~5× |
| XML — peak process memory vs a SimpleXML loop | 55 MB | 692 MB (SimpleXML) | ~12× |
| XML — peak process memory vs SimpleXML, all rows collected | 55 MB | 997 MB (SimpleXML) | ~18× |
Don't take these numbers on faith — run them
Every figure on this page comes from a public, runnable benchmark project: identical DTO shapes for both libraries, inside a booted Laravel app, with each library's cache warmed first. Clone it, read the code, swap in your own payloads. Your absolute numbers will differ with hardware — the ratios are what to compare.
Open the benchmark repository →git clone https://github.com/std-out/simple-data-objects-benchmark
cd simple-data-objects-benchmark
make bench # everything, in Docker (PHP 8.4)
make bench-xml # only the XML feed comparison
