NateTheGreatt/bitECS

Performance issues

Closed this issue ยท 8 comments

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%.

I also noticed that System#remove is pretty slow. The reason being the splice call. By changing it to a swap pop removal, it saves a significant amount of time. A trivial implementation could be:

localEntities[index] = localEntities[localEntities.length - 1];
localEntities.pop();

However it would mess up the query iteration.

You are awesome. Thanks for pointing all of this out! I'm going to spend some time investigating these findings when I can.

@ooflorent

bitECS v0.1.3 has been released with a potential fix for this! Oddly enough, what fixed it for me was changing the backwards iteration through the localEntities of a system over to a simple call to .forEach. Luckily, this also circumvents the issue of removals during system execution.

Let me know if this fixes your benches!

@ooflorent

I committed patch 258a7b6 that should significantly increase remove performance. I added a map of eid -> index that eliminates the need for an indexOf() call and switched the splice() to a swap-pop removal on the localEntities.

I also updated the tests to reflect these performance changes and all 170 current tests are passing. Version 0.1.4 has been published to npm. Let us know how it performs in your benchmarks and feel free to close this issue if you feel like the issues have been resolved.

Thanks for diving in with us!

I'm missing something here. It looks like your patch is not included in v0.1.4:

(y.remove = (e) => {
  const t = m.indexOf(e);
  -1 !== t &&
    (m.splice(t, 1), u.set(e, !1), (y.count = m.length), h && h(e));
});

@ooflorent i botched the publish lol. sorry about that. version 0.1.6 should be good to go, just verified the publication myself

I've updated the benchmarks (noctjs/ecs-benchmark#19).
Iteration is now 20% faster.

awesome. thanks again @ooflorent. closing this out