The ECS Benchmark: Koru Beats Bevy on Eleven of Twelve Workloads
Koru has no entities, no world, no archetypes, no query system. It has std/store — a column of values declared beside the program, striped, sized at compile time — and a tor query that sweeps it. And that is enough to beat the industry’s ECS framework at its own benchmarks.
This post is the full table from the ECS surface benchmark: twelve workloads, three implementations, one machine, one harness. Koru wins eleven of the twelve against Bevy ECS. It also, honestly, loses several against a hand-tuned Zig baseline — a faster thing exists, written by someone who tried. Both sides go in.
The world is a named column
Here is the world dense runs — a 100,000-row store of position and velocity, declared once:
std/store:new(bodies, capacity: 100000) { px: f32, py: f32, vx: f32, vy: f32, hp: i64, act: i64 } And the system — a tor that queries the store and writes px += vx back:
tor integrate {}
integrate = std/store:query(bodies)
! query e |> std/store:stored { e.px: e.px + e.vx, e.py: e.py + e.vy } That is the whole shape. No scheduler to register with. No system to add to a world. The query is the system; the sweep is the schedule. A store that a workload never touches costs its sweeps nothing — each column lives at a static address and the others sit untouched beside it.
The sparse variant is the same query with a guard:
tor integrate-active {}
integrate-active = std/store:query(bodies)
! query e when e.act == 1 |> std/store:stored { e.px: e.px + e.vx, e.py: e.py + e.vy } That guard is where the honest story starts, because Koru has no index. The baseline walks a 10%-dense index and touches only the active rows; Koru sweeps every row and filters. The gap between the two sparses is real, and this post reports it instead of hiding it.
The table
One fresh run of the harness on this machine (Apple M2 Pro, macOS), each arm built from its own source. Times in microseconds, lower is better. = marks a scenario whose sink — a deterministic checksum of the work done — is bit-identical across all three implementations, so the arms are provably computing the same thing.
| scenario | zig_striped | bevy_ecs | koru_store | vs bevy | agreed sink |
|---|---|---|---|---|---|
| schedule_empty | 32 | 1,398,420 | 2.1 | — | = |
| add_remove | 39 | 13,370 | 385 | 35× | = |
| query_get | 1,402 | 40,363 | 1,730 | 23× | = |
| spawn_batch | 238 | 4,122 | 511 | 8.1× | = |
| spawn | 266 | 4,013 | 763 | 5.3× | = |
| despawn | 262 | 4,219 | 1,103 | 3.8× | = |
| bevy_strength_world | 27,565 | 63,064 | 16,661 | 3.8× | = |
| dense | 2,476 | 10,111 | 2,799 | 3.6× | = |
| boids | 235,067 | 347,750 | 101,453 | 3.4× | = |
| combat_world | 3,706 | 8,327 | 6,940 | 1.2× | = |
| fanout | 8,512 | 26,704 | 13,451 | 2.0× | ✗ |
| sparse | 2,035 | 2,845 | 4,450 | 0.64× | = |
bevy_strength_world is the scenario the harness description says is “intended to favor Bevy ECS” — dense archetypes, dynamic bodies, command cleanup, changed-component checksums. Koru is the fastest of the three, at a checksum bit-identical to both. boids is a borrowed Unity DOTS flocking workload; naive Koru beats Bevy here by 3.4× and sits inside the range of hand-written -O3 C — the subject of the earlier ECS benchmark post.
The two honest footnotes:
sparseis a loss against Bevy, not a rounding error — 0.64×. It is the one workload where the guard’s full sweep shows. The README for the benchmark says so in exactly that register: “That gap is the measurement, not a detail to hide.”fanoutdoes not compare like-for-like. The arms damage different victim rows because handle-addressing makes the baseline’s index access unspellable in Koru. Bevy’s sink already differed from the baseline’s before Koru existed. Its 2.0× is real measured time, but it is not the same computation, so it is the one row without sink agreement.
What schedule_empty actually means
Koru’s schedule_empty is 2 microseconds for 100,000 frames: the schedule resolves at compile time, the loop folds, and there is no per-system dispatch to pay. Bevy pays 1.4 seconds for the same 100,000 empty frames because a scheduled system is a runtime object with a real call.
This is the most misleading ratio in the table, so it deserves the plainest framing. Koru is not “660,000× more efficient at scheduling” — it is that calling an empty Koru system is not the same operation as calling an empty Bevy system. Bevy does real work Bevy decides at runtime; Koru did it at compile time. That is the whole difference, and it is why the number is so large and why it means less than it looks.
The humbler boundary: a hand-tuned baseline still wins
Against Bevy, the mainstream, Koru wins. Against the Zig baseline that the harness ships — a straight-line, hand-tuned, idiomatic implementation of the same workloads — Koru wins only schedule_empty, bevy_strength_world, and boids, and loses the rest. The correct reading is not “Koru is fastest,” it is the README’s own sentence:
Koru is not the fastest thing in this table; it is the fastest thing nobody had to try to write.
Koru beats the framework you would otherwise reach for, by margins the workloads are actually made of. It does not beat the expert who tuned one shape by hand for a week. Both statements are true, and a performance post that only printed the first would be lying by omission.
Why
The wins are not a performance feature. They fall out of the design for unrelated reasons, and the earlier post argues the strongest case: Koru’s emitted loop sits at codegen parity with clang because three things Koru does anyway — module-level columns at static addresses, bounds checks waived at the declaration, per-component selects — happen to be the three preconditions a vectoriser checks. None of them was a decision about speed; all three were decisions about the language, and the speed came along.
The honest arithmetic is this: an ECS in Koru is a named column and a query. The writer thinks about the data as columns, and the columns are the thing that makes the code fast. There is no scheduler to pay, no archetype to keep coherent, no system registry to consult. The benchmark is the proof that those are costs, not features.