Last night I decided that I wanted to gain a better understanding of how heavy it is to work “oop-style” in DOTS, when compared to working with regular class refs in c#. In other words; How costly is it to iterate on components using an entity array and a “ComponentDataFromEntity”, instead of iterating on classes in a list. I wanted to know this, because there are some problems sometimes that are difficult to implement in a perfectly data-oriented way (for example: transferring the inputs of Player/AI controller entities to their controlled entities). And I wanted to know if working with “FromEntity” was an acceptable backup plan in those cases
The test is this: we iterate on an array or List of 1 million classes/components, and for each one, we increment an int. This is run on a i7-9750H, in a build
Results:
Iterating a List of classes: 3.6ms
Iterating on a ComponentDataFromEntity in an IJob: 5.3ms
Iterating on a ComponentDataFromEntity in a IJobParallelFor: 1.3ms
Iterating on a NativeArray (data-oriented style) in an IJob: 2ms
Iterating using IJobForeach (data-oriented style): 0.25ms
And if instead of incrementing an int, we do a vector multiplication (to see the benefits of Mathematics/SIMD):
Results:
Iterating a List of classes: 21.5ms
Iterating on a ComponentDataFromEntity in an IJob: 6.5ms
Iterating on a ComponentDataFromEntity in a IJobParallelFor: 1.9ms
Iterating on a NativeArray (data-oriented style) in an IJob: 7ms
Iterating using IJobForeach (data-oriented style): 0.85ms
This is, of course, an overly-simplistic test. So take it for what it’s worth. But I still think it’s cool and useful to understand the cost of “FromEntity” iteration a little bit better. My conclusion is that a single-threaded iteration using “FromEntity” is more costly, but as soon as the problem is parallelizable (or as soon as some math is involved), it becomes more efficient than class refs
There is, however, something that baffles me in the second test: the single-thread “FromEntity” version is more efficient than the single-thread “data-oriented” version. Maybe it’s the cost of doing the “ToComponentDataArray”?
source code of test is attached. Maybe I made a dumb mistake somewhere
Just adding my tests (best numbers for 100 runs) on a Ryzen 7 3700x:
Results:
Iterating a List of classes: 2.5ms
Iterating on a ComponentDataFromEntity in an IJob: 5.4ms
Iterating on a ComponentDataFromEntity in a IJobParallelFor: 0.8ms
Iterating on a NativeArray (data-oriented style) in an IJob: 0.6ms
Iterating using IJobForeach (data-oriented style): 0.18ms
Iterating a List of classes (randomly sorted at creation): 11.6ms
So, in bold is an additional test with a copy from the original “List of classes” sorted in a randomly way, at creation. The reason for this test is instantiating classes at sequence, especially at the beginning, will give a good chance that they will be nice aligned and close in memory. The GC don’t move objects in Unity, so you can use this as a “trick” to an certainly extent but in real world scenario your instances will be all over the place. So this help testing the worst case scenario.
Results:
Iterating a List of classes: 21.5ms
Iterating on a ComponentDataFromEntity in an IJob: 6.5ms
Iterating on a ComponentDataFromEntity in a IJobParallelFor: 1.9ms
Iterating on a NativeArray (data-oriented style) in an IJob: 7ms
Iterating using IJobForeach (data-oriented style): 0.85ms
Calling ToComponentDataArray will create a new array and copy all the data (using jobs). For direct access you need to use chunk iteration.
These numbers make perfect sense. ComponentDataFromEntity does two random accesses instead of one. One to look up the chunk the entity is in, and another to look up the entity in the chunk. If this becomes a performance bottleneck for a really random access heavy algorithm, it may be worth baking the data into NativeArrays first.