While working a benchmark suite, I figured that bitecs
is not as performant as it may seem or be. I believe there is dramatically polymorphic code in some hot blocks.
Let's consider the following setup:
let world = bitECS();
world.registerComponent("A", { value: "int32" });
world.registerComponent("B", { value: "int32" });
world.registerSystem({
name: "AB",
components: ["A", "B"],
update: (a, b) => (eid) => {
a.value[eid] += b.value[eid];
},
});
for (let i = 0; i < 1_000; i++) {
let e = world.addEntity();
world.addComponent("A", e, { value: 0 });
world.addComponent("B", e, { value: 0 });
}
No surprise here, bitecs
is in fact the fastest ECS implementation at doing this: looping over 1,000 entities with 2 components. world.step()
can be run at 550,648 op/s!
I decided to add a second system:
world.registerSystem({
name: "A2",
components: ["A"],
update: (a) => (eid) => {
a.value[eid] *= 2;
},
});
The second query would traverse the same number of entities, so I expected the operation count to be divided by two. Instead I got 61,925 op/s. That's a 90% performance drop. Way below what one could expect.
By rewriting System#execute
, I was able to fix the performance drop. However, I must warn you, it looks bad.
let executeTemplate = (
system,
localEntities,
componentManagers,
updateFn,
before,
after
) => (force) => {
if (force || system.enabled) {
if (before) before(...componentManagers);
if (updateFn) {
const to = system.count;
for (let i = to - 1; i >= 0; i--) {
const eid = localEntities[i];
updateFn(eid);
}
}
if (after) after(...componentManagers);
}
};
let executeFactory = Function(`return ${executeTemplate.toString()}`)();
system.execute = executeFactory(
system,
localEntities,
componentManagers,
updateFn,
before,
after
);
I also moved the updateFn
check. It saves another 10%.