[ANN] Ark.jl v0.3.0: Archetype-based ECS, now with entity relationships and batch operations

Ark.jl v0.3 is our biggest feature release yet. It introduces first‑class entity relationships, expands batch operations far beyond entity creation, and delivers substantial performance improvements.

Why ECS?

Skip this of you know it already!

Entity Component Systems (ECS) offer a clean, scalable way to build individual- and agent-based models by separating agent data from behavioral logic. Agents are simply collections of components, while systems define how those components interact, making simulations modular, extensible, and efficient even with millions of heterogeneous individuals.

Ark.jl brings this architecture to Julia with a lightweight, performance-focused implementation that empowers scientific modellers to design complex and performant simulations without the need for deep software engineering expertise.

Release highlights

Entity relationships

This release adds first‑class support for entity relationships, allowing you to express connections between entities directly using ECS primitives. While it is possible to express relations by storing entities inside components, the tight integration into the ECS provides several benefits. Most importantly, relationship can be queried now as efficiently as component queries. In addition, relationships become more ergonomic, more consistent, and safer to use.

For details, see the user manual’s chapter on Entity relationships.

Batch operations

Previous versions of Ark.jl already offered blazing‑fast batch entity creation. This release generalizes the concept to all operations that modify entities or their components. You can now remove all entities matching a filter, add components to all matching entities, and more, using a single batched call. These operations are typically at least an order of magnitude faster than performing the same changes individually.

For details, see the user manual’s chapter on Batch operations

Cached queries

Queries in archetype‑based ECS are already highly efficient, but this release introduces cached queries for even greater performance, especially in worlds with many archetypes. Instead of checking the components of all archetypes in the pre-selection (which is based on the most β€œrare” component in a query), cached queries maintain a list of all matching archetypes. This means matching checks are only needed when a new archetype is created, eliminating overhead during query iteration.

Performance improvements

Numerous optimizations to component operations and the archetype graph yield significant speedups. Component operations are now 1.5–2Γ— faster, and entity creation is up to 3Γ— faster than before.

More

For a full list of all changes, see the CHANGELOG.

As always, your feedback contributions are highly appreciated!

10 Likes

Looks very impressive, congratulations on the significant achievements since the initial release. Based on the description of how relationships work in the docs (1:1 between entities), it seems they are functions in the strict mathematical sense. This naturally leads me to think of Ark as being somewhat like an in-memory relational database, with the relations functioning like foreign keys. Is this how you think of Ark/ECS when using the system in designing β€œreal” simulations/projects? Also, I am curious if you have seen GitHub - AlgebraicJulia/ACSets.jl: ACSets: Algebraic databases as in-memory data structures before which takes a categorical interpretation to the β€œsets and functions” approach to in memory relational databases.

3 Likes

I read some of the discussions on the topics on the internet inspired by your question, and I think that it is (somewhat) of a shared interpretations that ECS can be thought as an in-memory relational database. Though, in respect to other kinds of in-memory databases it is mostly optimized for raw speed and flexibility in operations related to simulations/games. In the end, queries in Ark just iterates over homogeneous arrays so the speed is sort of as good as it can get on these operations.

I find a bit hard to understand what ACSets does since the algebraic viewpoint is a bit unfamiliar to me and at the same time the documentations is very brief. What I can see is that the performance can’t most probably match the one in Ark since its API is not enough β€œtype-based” so it is probably type unstable in some places, but I have to say I don’t really understand how to write an equivalent Ark program with ACSets.

I now tried to replicate the simple position-velocity ECS benchmark with ACSets.jl, this is what I got:

pos-vel with ACSets
using ACSets

const SchPhysics = BasicSchema(
	  [:Entity],
	  [],
	  [:Real],
	  [
	    (:x,  :Entity, :Real),
	    (:y,  :Entity, :Real),
	    (:dx, :Entity, :Real),
	    (:dy, :Entity, :Real)
	  ]
	)

@acset_type PhysicsWorld(SchPhysics, index=[])

function run_posvel_acsets()
	world = PhysicsWorld{Float64}()

	n = 10^5
	add_parts!(world, :Entity, n, 
	  x  = Float64[i for i in 1:n], 
	  y  = Float64[i * 2 for i in 1:n],
	  dx = ones(Float64, n), 
	  dy = ones(Float64, n)
	)

	for t in 1:10
	    xs  = world[:x]
	    ys  = world[:y]
	    dxs = world[:dx]
	    dys = world[:dy]
	    @inbounds for i in eachindex(xs)
	        xs[i] = xs[i] + dxs[i]
	        ys[i] = ys[i] + dys[i]
	    end
	end

	return world
end

which when we benchmark it we get

julia> @benchmark run_posvel_acsets()
BenchmarkTools.Trial: 22 samples with 1 evaluation per sample.
 Range (min … max):  167.329 ms … 351.249 ms  β”Š GC (min … max):  2.46% … 47.08%
 Time  (median):     199.468 ms               β”Š GC (median):     5.90%
 Time  (mean Β± Οƒ):   229.912 ms Β±  67.248 ms  β”Š GC (mean Β± Οƒ):  22.50% Β± 20.14%

  ▁  β–„      β–ˆ                                         β–„          
  β–ˆβ–†β–β–ˆβ–β–β–β–†β–†β–β–ˆβ–†β–†β–β–†β–†β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–ˆβ–β–β–†β–β–β–†β–β–† ▁
  167 ms           Histogram: frequency by time          351 ms <

 Memory estimate: 227.00 MiB, allocs estimate: 11970103.

instead

pos-vel with Ark
using Ark

struct Position
    x::Float64
    y::Float64
end

struct Velocity
    dx::Float64
    dy::Float64
end

function run_posvel_ark()
	world = World(Position, Velocity)

	new_entities!(world, 10^5, (Position, Velocity)) do (entities, positions, velocities)
      	for i in eachindex(entities)
          	positions[i] = Position(i, i * 2)
          	velocities[i] = Velocity(1, 1)
      	end
  	end

	for i in 1:10
	    for (entities, positions, velocities) in Query(world, (Position, Velocity))
	        @inbounds for i in eachindex(entities)
	            pos = positions[i]
	            vel = velocities[i]
	            positions[i] = Position(pos.x + vel.dx, pos.y + vel.dy)
	        end
	    end
	end

	return world
end

gives

julia> @benchmark run_posvel_ark()
BenchmarkTools.Trial: 4582 samples with 1 evaluation per sample.
 Range (min … max):  721.455 ΞΌs …   5.888 ms  β”Š GC (min … max): 0.00% … 41.52%
 Time  (median):       1.014 ms               β”Š GC (median):    0.00%
 Time  (mean Β± Οƒ):     1.086 ms Β± 217.248 ΞΌs  β”Š GC (mean Β± Οƒ):  9.77% Β± 12.18%

                 β–β–β–ˆβ–†β–‚                                           
  β–β–β–β–β–‚β–‚β–‚β–‚β–‚β–‚β–‚β–‚β–‚β–ƒβ–…β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‡β–…β–…β–…β–…β–…β–…β–…β–„β–„β–ƒβ–‚β–ƒβ–‚β–‚β–‚β–‚β–ƒβ–ƒβ–ƒβ–ƒβ–ƒβ–ƒβ–ƒβ–ƒβ–„β–„β–ƒβ–ƒβ–‚β–‚β–‚β–‚β–‚β–‚β–‚β–‚β–‚β–β–β– β–ƒ
  721 ΞΌs           Histogram: frequency by time         1.55 ms <

 Memory estimate: 7.17 MiB, allocs estimate: 442.

so it seems more than 200x slower in this simple benchmark.

In general while trying the library it seems also more restrictive than Ark, e.g. I’m not getting how you could add/remove components to entities at runtime.

1 Like