Chris3606 / GoRogue

.NET Standard roguelike library in C#. Features many algorithms and data structures pertinent to roguelike/2D game developers, specifically designed to be minimally intrusive upon the developer's architecture.
MIT License
494 stars 31 forks source link

Consider Removing IComponentCollection (v4) #299

Open Chris3606 opened 1 year ago

Chris3606 commented 1 year ago

Currently, the component system in GoRogue has IComponentCollection, which is really just an interface used for an implementation of a collection of components. It also provides ComponentCollection, which is a concrete implementation of that interface.

However, it may be worth considering removing IComponentCollection and simply providing ComponentCollection instead. I'm not aware of very many use cases where a custom implementation of IComponentCollection is the correct answer to a problem, since ComponentCollection is quite efficient and versatile. Futhermore, if ComponentCollection.GetAll<T> uses a custom enumerator (which is coming in a PR soon), accessing GetAll through an interface reference is actually significantly slower, since it requires boxing.

If there is a compelling use case for IComponentCollection, I will happily leave it; but I simply haven't seen a use case that I can recall where it solved a problem; and if it solves a non-existent problem and also costs performance, it would make sense to remove it.

Chris3606 commented 1 year ago

Adding some performance tests that back this up:

The version of ComponentCollection in beta5 uses a custom iterator for GetAll. The following benchmarks that implementation:

Method TypesOfComponents ComponentsPerType Mean Error StdDev
GetAllMockA 1 2 40.04 ns 0.808 ns 0.930 ns
GetAllMockAViaInterface 1 2 92.37 ns 0.324 ns 0.270 ns
GetAllIMock 1 2 41.74 ns 0.423 ns 0.375 ns
GetAllIMockViaInterface 1 2 99.77 ns 1.441 ns 1.203 ns
GetAll2MockA 1 2 57.40 ns 1.104 ns 1.133 ns
GetAll2MockAViaInterface 1 2 67.15 ns 0.460 ns 0.384 ns
GetAll2IMock 1 2 63.50 ns 1.108 ns 1.037 ns
GetAll2IMockViaInterface 1 2 77.80 ns 0.367 ns 0.286 ns
GetAllMockA 1 10 62.71 ns 0.242 ns 0.202 ns
GetAllMockAViaInterface 1 10 204.15 ns 0.646 ns 0.539 ns
GetAllIMock 1 10 87.27 ns 0.512 ns 0.427 ns
GetAllIMockViaInterface 1 10 241.40 ns 0.895 ns 0.747 ns
GetAll2MockA 1 10 140.73 ns 0.703 ns 0.623 ns
GetAll2MockAViaInterface 1 10 150.34 ns 1.819 ns 1.702 ns
GetAll2IMock 1 10 174.75 ns 0.571 ns 0.506 ns
GetAll2IMockViaInterface 1 10 193.57 ns 0.297 ns 0.232 ns
GetAllMockA 2 2 37.46 ns 0.181 ns 0.160 ns
GetAllMockAViaInterface 2 2 91.72 ns 1.025 ns 0.959 ns
GetAllIMock 2 2 59.91 ns 0.999 ns 0.934 ns
GetAllIMockViaInterface 2 2 137.84 ns 0.317 ns 0.248 ns
GetAll2MockA 2 2 56.13 ns 0.259 ns 0.229 ns
GetAll2MockAViaInterface 2 2 67.87 ns 0.703 ns 0.587 ns
GetAll2IMock 2 2 93.69 ns 0.846 ns 0.750 ns
GetAllMockA 2 10 58.19 ns 0.157 ns 0.123 ns
GetAllMockAViaInterface 2 10 202.02 ns 0.342 ns 0.286 ns
GetAllIMock 2 10 167.17 ns 0.422 ns 0.374 ns
GetAllIMockViaInterface 2 10 426.41 ns 2.096 ns 1.750 ns
GetAll2MockA 2 10 141.43 ns 1.630 ns 1.525 ns
GetAll2MockAViaInterface 2 10 147.20 ns 1.177 ns 1.043 ns
GetAll2IMock 2 10 313.42 ns 3.609 ns 3.199 ns
GetAll2IMockViaInterface 2 10 339.23 ns 1.494 ns 1.324 ns

"GetAll" represents the current, custom enumerator version, both called via a concrete variable of type ComponentCollection, and on that same instance but via an interface. The GetAll2 implementation is an alternate implementation which uses yield return, rather than a custom enumerator, which keeps the interface version closer time-wise, but is slower overall. In both cases, however, the interface is slower.