Hello guys.
I recently ran a comparison between ReactiveECS.jl (RECS) and Overseer.jl, two powerful ECS (Entity-Component-System) frameworks. The results are eye-opening and show that both frameworks have unique strengths for game development and simulations. Here’s a comparison of their performance, features, and use cases, complete with benchmarks and code for you to try out!
Context
I tested a simple PhysicSystem
performing a translation on a fixed number of entities, each with two components: Transform
and Physic
. The goal was to compare the speed and memory efficiency of RECS and Overseer for a common game engine task.
Test configuration:
- OS: Windows 10
- CPU: Intel Pentium T4400 @ 2.2 GHz
- RAM: 2 GB DDR3
- Julia: v1.10.5
Benchmarks
Here are the results for the translation task across different entity counts:
Entity Count | Overseer | RECS (without vectorization) | RECS (with vectorization) |
---|---|---|---|
10 | 521 ns | 13.9 µs | 14.9 µs |
64 | 1.96 µs | 17.4 µs | 17.7 µs |
100 | 2.86 µs | 13.5 µs | 18.2 µs |
1,000 | 27.53 µs | 78.4 µs | 48.1 µs |
10,000 | 265 µs | 271.1 µs | 97.6 µs |
100,000 | 2.78 ms | 606.7 µs | 163.8 µs |
Allocations
- Overseer : 0 allocations
- RECS : 6 allocations
These allocations are constant across the entities count.
Key observations:
- Overseer shines for small entity counts (<4K), with lower latency (e.g., 521 ns vs 13.9 µs for 10 entities).
- RECS takes the lead for larger entity counts (>4K), with 163.8 µs vs 2.78 ms for 100K entities (~17x faster with vectorization).
- Even at low entity counts, RECS’s 13-80 µs is negligible compared to a 60 FPS frame budget (~16.6 ms), making both frameworks viable for games.
Entity creation:
- 1 entity:
- Overseer: 1.587 µs (4 allocations)
- RECS: 1.4 µs (8 allocations)
- RECS (allocate entity): 864 ns (2 allocations)
- 10K entities:
- Overseer: ~20 ms
- RECS: ~3 ms
- RECS outperforms Overseer for entity creation, especially at scale.
Entity destruction:
- Overseer (
schedule_delete!
+delete_scheduled
): 2 µs - RECS: 161 ns
- Both scale linearly (O(n)), but RECS is ~12x faster for destruction.
These result shows that RECS outperforms Overseer at scale. While Overseer ourperforms RECS for a lower number of entities, especially for processing
Test Code
Here’s the code I used for the benchmarks, so you can reproduce the results:
ReactiveECS.jl
using ReactiveECS
using LoopVectorization
using BenchmarkTools
COUNT = 100_000
@component Transform begin
x::Float32
y::Float32
end
@component Physic begin
velocity::Float32
end
@system PhysicSystem begin
delta::Float32
end
@system RenderSystem
function ReactiveECS.run!(world, sys::PhysicSystem, data)
E = world[sys]
indices::Vector{Int} = data.value
L = length(indices)
transforms = E.Transform
physics = E.Physic
x_pos = transforms.x
velo = physics.velocity
dt::Float32 = sys.delta
@inbounds for i in indices
x_pos[i] += velo[i]*dt
end
return transforms
end
world = ECSManager()
phys_sys = PhysicSystem()
subscribe!(world, phys_sys, (TransformComponent, PhysicComponent))
run_system!(phys_sys)
# Create 100K entities
request_entities(world, COUNT, (TransformComponent, PhysicComponent))
# Benchmark
@btime begin
dispatch_data(world)
blocker(world)
end
Overseer.jl
using Overseer
using BenchmarkTools
COUNT = 100_000
@component struct Transform
x::Float64
y::Float64
end
@component struct Physic
vx::Float64
end
struct PhysicSystem <: System end
Overseer.requested_components(::PhysicSystem) = (Transform, Physic)
function Overseer.update(::PhysicSystem, m::AbstractLedger)
for e in @entities_in(m, Transform && Physic)
e[Transform] = Transform(e.x + e.vx, e.y)
end
end
m = Ledger(Stage(:sim, [PhysicSystem()]))
for i=1:COUNT
Overseer.Entity(m,
Health(50),
Transform(-5.0, 5.0),
Physic(1.0)
)
end
@btime Overseer.update(m)
Strengths of ReactiveECS.jl
RECS is designed for high performance and reactive modularity, making it ideal for large-scale games or simulations.
- Blazing performance:
- Uses a Struct of Arrays (SoA) layout for cache-friendly access.
- Leverages
LoopVectorization.jl
(@turbo
,@simd
): the test shows 163.8 µs for 100K entities. - Entity pooling (e.g., 161 ns for destruction) minimizes allocation overhead.
- Reactive modularity:
- The
listen_to
mechanism enables dynamic system pipelines, where a system can listen to the output of another. get_into_flow
allows runtime system injection, perfect for evolving game mechanics or modding.- Supports hierarchies via
NodeTree.jl
and advanced events viaNotifyers.jl
(merge, filtering, priorities).
- The
Drawback: The package is slower than Overseer for lower entity count, mostly due to systems’s synchronization with blocker(ecs)
.
Strengths of Overseer.jl
Overseer is a robust, user-friendly ECS inspired by EnTT, prioritizing determinism and simplicity with solid performance.
- Solid performance:
- Uses contiguous vectors and
SparseIntSet
for efficient iteration, achieving 2.78 ms for 100K entities with 0 allocations in the test. - Scales well for small to medium entity counts (<4K).
- Uses contiguous vectors and
- Simplicity:
- The
@entities_in
macro is intuitive, making it accessible to ECS newcomers.
- The
- Maturity: Used in projects like Glimpse.jl and Trading.jl, Overseer is more battle-tested than RECS, with clear documentation.
- Use case: Perfect for rapid prototyping, small-to-medium games, or simulations where determinism and ease of use are key.
Drawback: Performance drops at large scales due to lack of SIMD vectorization or reactive pipelines, limiting flexibility for dynamic systems.
Discussion
- Choose RECS for high-performance games or simulations with large entity counts (100K+) and dynamic, real-time systems. Its speed and modularity make it a game-changer for ambitious projects.
- Choose Overseer for simpler, deterministic projects, where ease of use is a priority.