3 ms·
My first use of Go generics has been for a concurrent "ECS" game engine. In this case, the gains are pretty obvious. I think. I get to write one set of generi
by pphysch 5y ago
My first use of Go generics has been for a concurrent "ECS" game engine. In this case, the gains are pretty obvious. I think.
I get to write one set of generic methods and data structures that operate over arbitrary "Component" structs, and I can allocate all my components of a particular type contiguously on the heap, then iterate over them with arbitrary, type-safe functions.
I can't fathom that doing this via a Component interface would be even as close as fast, because it would destroy cache performance by introducing a bunch of Interface tuples and pointer dereferencing for every single instance. Not to mention the type-unsafe code being yucky. Am I wrong?
FWIW I was able to update 2,000,000 components per (1/60s) frame per thread in a simple Game of Life prototype, which I am quite happy with. But I never bothered to evaluate if Interfaces would be as fast
- folago 5y agoSounds interesting, is it available somewhere?
- pphysch 5y agoStill want to hit some milestones before releasing anything, so not quite
- siftrics 5y agoAssuming your generic functions take _pointers_ to Components as input, full monomorphization does not occur and you're suffering a performance hit similar in magnitude, if not strictly greater empirically, to interface "dereferences". On this basis, I don't believe your generic implementation is as faster than an interface implementation as you claim.
- pphysch 5y agoYou're right, here's what one of my hot loops look like: func (cc *ComponentContainer[T]) ForEach(f func(*Component[T])) { for _, page := range cc.pool.pages { for i := range page { if page[i].IsActive() { f(&page[i]) } } } } Still, the interface approach is a total nightmare from a readability + runtime error perspective so I won't be going back & will just hope for some performance freebies in 1.19 or later :^)
- Groxx 5y agoJust to clarify things here: a pointer receiver on a generic type isn't what was covered in the article. That was covering if your `T` is a pointer type. If T here is a value-type, you're probably getting monomorphization (at the cost of copying the value all over, and possible benefit of inlining to reduce that copying).
- pphysch 5y agoThanks, that makes more sense! My components are all small structs (coordinates, states, health values, etc) so I am happy to copy them around Component[T] is something like { Metadata_fields... Data T }
- deleted 5y ago[deleted]
- nikki93 5y agoSweet! I've been using it for the same. Example game project (did it for a game jam): https://github.com/nikki93/raylib-5k https://github.com/nikki93/raylib-5k -- in this case the Go gets transpiled to C++ and runs as WebAssembly too. Readme includes a link to play the game in the browser. game.gx.go and behaviors.gx.go kind of show the ECS style. It's worked with 60fps performance on a naive N^2 collision algo over about 4200 entities -- but also I tend to use a broadphase for collisions in actual games (there's an "externs" system to call to other C/C++ and I use Chipmunk's broadphase).