jonascarpay / apecs

a fast, extensible, type driven Haskell ECS framework for games
392 stars 43 forks source link

Add Apecs.Stores.Tags (unsafeCoerce ()) #106

Closed dpwiz closed 1 year ago

dpwiz commented 1 year ago

Kinda like Filter pseudo-component (skips lookup), but as a Store for arbitrary unit-like components.

jonascarpay commented 1 year ago

This is great, I think it makes a lot of sense to have some concept of unit values that are efficiently backed by an IntSet or equivalent.

dpwiz commented 1 year ago

My aim was to minimize ritual required to use this.

This PR has it in a most straight-forward way. Nothing is required (not even Rep test) since nothing is checked. It should be safe unless you do more reflection/inspection/pointer equality/etc.

The alternative PR has it safe-r, but requires some deriving or typing the obvious boilerplate. Benchmarks show no noticeable difference, being in ns-range.

There can be another way of course. I was just banging on the keyboard and making stores to see if I can beat cached IntMap in some non-exotic case. That thing is pretty damn fast :sweat_smile:

dpwiz commented 1 year ago

I had a dive into google just now to find out how components are named in different engines/packages.

The store may as well be named Set to follow its Map/IntMap counterpart, I just wanted to experiment with a different scheme. One may argue that specifying the underlying structure is more relevant to the storage definition than its intended use: type Storage Fizz = Tags Fizz vs type Storage Fizz = Set Fizz.

The class name what would serve as a guard can be named:

jonascarpay commented 1 year ago

Thanks for the research, let's go with Tag then.

The store may as well be named Set to follow its Map/IntMap counterpart

Agreed

The alternative PR has it safe-r, but requires some deriving or typing the obvious boilerplate.

Eh, I think the boilerplate is fine, having deriving (Generic, Tag) shouldn't be too big of an issue. Maybe we could do something like deriving (Tag, Component) via AsTag MyTag, and then it pays for itself in terms of line count.

There can be another way of course. I was just banging on the keyboard and making stores to see if I can beat cached IntMap in some non-exotic case.

Hmm, that's a good point, it probably won't be faster than a cache anyway... I suppose we could make a tag-specific cache that doesn't allocate the element vector, but that seems a bit excessive...

dpwiz commented 1 year ago

Set-based store beats Map-based by roughly 7x.

Surprisingly, using Filter component changes almost nothing.

tags
  unsafe
    1:   OK (0.72s)
      158  ns ± 7.5 ns
    100: OK (0.17s)
      314  ns ±  29 ns
    1k:  OK (0.66s)
      311  ns ±  11 ns
    10k: OK (0.18s)
      330  ns ±  31 ns
  generic
    1:   OK (0.20s)
      183  ns ±  18 ns
    100: OK (0.18s)
      332  ns ±  26 ns
    1k:  OK (0.35s)
      322  ns ±  20 ns
    10k: OK (0.18s)
      328  ns ±  31 ns
  map
    1:   OK (5.07s)
      2.22 μs ± 123 ns
    100: OK (5.16s)
      2.24 μs ± 127 ns
    1k:  OK (5.15s)
      2.24 μs ± 124 ns
    10k: OK (5.15s)
      2.23 μs ± 143 ns
  filter-map
    1:   OK (5.12s)
      2.22 μs ± 136 ns
    100: OK (5.11s)
      2.21 μs ± 124 ns
    1k:  OK (5.11s)
      2.22 μs ± 131 ns
    10k: OK (5.11s)
      2.22 μs ± 134 ns
dpwiz commented 1 year ago

Alllllright.... The culprit was the Cache wrapper.

1) The IntMap can hold its own candle. 2) The Filter actually works, beating Set.

map
  1:   OK (0.16s)
    146  ns ±  14 ns
  100: OK (0.18s)
    341  ns ±  25 ns
  1k:  OK (0.19s)
    357  ns ±  24 ns
  10k: OK (0.18s)
    336  ns ±  33 ns
filter-map
  1:   OK (0.24s)
    110  ns ± 9.3 ns
  100: OK (0.57s)
    267  ns ± 7.2 ns
  1k:  OK (0.15s)
    274  ns ±  23 ns
  10k: OK (0.15s)
    271  ns ±  24 ns

Both of the Tag stores are obsoleted by a documentation update :sweat_smile:

jonascarpay commented 1 year ago

Both of the Tag stores are obsoleted by a documentation update sweat_smile

I don't get it, in what way?

dpwiz commented 1 year ago

The benchmark shows no gain over uncached Map and using Filter is actually faster than both Set implementations.