ADR-0011: Keep the Mojo archetype ECS (benchmarked against flecs)
Status: accepted, 2026-11-08.
Context #
The brief requires an archetype ECS with typed queries, change detection, command buffers, hierarchy and access-scheduled systems, and asks for a benchmark against flecs through its iterator API: keep the Mojo ECS only if it is within 10% of flecs or faster.
Benchmark #
bench/ecs_bench.mojo (run with pixi run mojo build -I . bench/ecs_bench.mojo, then
ecs_bench 100000 100): 100,000 entities with Position and Velocity (8 bytes each), half of
them with a third component so both worlds hold two archetypes/tables; 100 frames of
position += velocity * dt over a two-component query. flecs 4.2.0 is driven through
ecs_query_iter/ecs_query_next/ecs_field_w_size (no callbacks), both sides from the same Mojo
process on the same core.
Container on the Windows host (lavapipe machine, Ubuntu 24.04, Mojo 1.2.0), three runs:
| Run | Mojo ns / entity-iteration | flecs ns / entity-iteration | ratio |
|---|---|---|---|
| 1 | 0.758 | 0.829 | 0.91 |
| 2 | 0.746 | 0.750 | 1.00 |
| 3 | 0.728 | 0.744 | 0.98 |
Two changes made the difference from the first measurement (1.13 ns, ratio 1.55):
- Column stride: elements were padded to 16-byte blocks, halving cache density for 8-byte components. The stride is now the component's size (a struct's size is a multiple of its alignment), while column bases stay 16-byte aligned for SIMD components.
- Typed column access:
Chunk.column[T]()returns a tight typed pointer indexed with[unsafe_offset=row], which the compiler vectorises like the flecs loop; byte-offset stepping with a runtime stride did not.
Decision #
Keep the Mojo ECS (cinder/ecs). It is within the 10% band (and slightly faster in every run), it
is the only implementation whose queries are typed at compile time with Mojo parameters, and its
scheduler integrates directly with the Mojo job system. flecs stays vendored and linked for the
benchmark so the comparison can be rerun whenever the ECS changes.
Consequences #
bench/ecs_bench.mojois part of the benchmark suite; a regression above the band reopens this decision.- Components are plain Mojo structs with a unique
component_id(); heap-owning components are supported through the type-erased move/destroy functions recorded at registration. - flecs' features the Mojo ECS does not have (relationships, reflection, the explorer) are not needed by the brief; the debug UI (Phase 7) inspects entities through the Mojo world directly.