A measure of performance of "class ref iteration" VS "ComponentFromEntity iteration"

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

5227886–521315–OOPTest.cs (5.24 KB)

2 Likes

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.

[ ]'s

4 Likes

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.

4 Likes