ReactiveECS vs Overseer: Reactive ECS is 4.6x faster than Overseer for 100k entities

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 via Notifyers.jl (merge, filtering, priorities).

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).
  • Simplicity:
    • The @entities_in macro is intuitive, making it accessible to ECS newcomers.
  • 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.
5 Likes