→ What is Actors
I have often heard how good the ECS template is, and that Jobs and Burst from the Unity library are the solution to all performance issues. In order not to add the word “probably” and “maybe” every time, discussing the speed of the code, I decided to check everything personally.
My goal was to make an open mind about how fast this development tool is and whether to use parallelization for calculations. And if it is, is it better to use Unity.Jobs or System.Threading ? At the same time I found out what is the use of ECS in real tasks.
Test conditions (close to real game tasks):
- I5 2500 processor (4 cores without hyper trading) and Unity2019.3.0f1
- Every GameObject every frame ...
A) moves along a quadratic Bezier curve for 10 minutes from the start point to the end.
B) calculates its square collider (box 10f10f), which uses math.sincos, math.asin, math.sqrt (the same, quite complicated calculations for all tests).
- Objects before FPS measurements are set at random positions within the 720fx1280f zone and move to a random point in this zone.
- Everything is tested in release in IL2CPP on PC
- Tests are recorded a few seconds after the launch, so that all initial preliminary calculations and the inclusion of Unity systems do not affect the FPS. For the same reasons, only the update code of each frame is shown.
- Objects do not have a visual display in the release, so that the rendering does not affect FPS.
Testing Positions and Update Code
- MonoBehaviour sequential (conditional marking).
The MonoBehaviour script is "hung" on the object, in the update of which the position, the collider is calculated and the self is moved.
Update codevoid Update() { // var velocityToOneFrame = velocityToOneSecond * Time.deltaTime; observedDistance += velocityToOneFrame; var t = observedDistance / distanceFull; if (t > 1f) t = 1f; var newPos = t.CalculateBesierPos(posToMove.c0, posToMove.c2,posToMove.c1); // obj.properties.c0 = newPos; var posAndSize = new float2x2 { c0 = newPos, c1 = obj.collBox.posAndSize.c1 }; obj.collBox = obj.entity.NewCollBox(posAndSize, new float2(10f, 10f), obj.rotation.ToEulerAnglesZ()); // tr.position = new Vector3(newPos.x, newPos.y); #if UNITY_EDITOR DebugDrowBox(obj.collBox, Color.blue, Time.deltaTime); #endif }
- Actors sequential on component classes without parallelization.
Update codepublic void Tick(float delta) { foreach (ent entity in groupMoveBezier) { var cMoveBezier = entity.ComponentMoveBezier_noJob(); var cObject = entity.ComponentObject(); ref var obj = ref cObject.obj; // var velocityToOneFrame = cMoveBezier.velocityToOneSecond * delta; cMoveBezier.observedDistance += velocityToOneFrame; var t = cMoveBezier.observedDistance / cMoveBezier.distanceFull; if (t > 1f) t = 1f; var newPos = t.CalculateBesierPos(cMoveBezier.posToMove.c0, cMoveBezier.posToMove.c2,cMoveBezier.posToMove.c1); // obj.properties.c0 = newPos; var posAndSize = new float2x2 { c0 = newPos, c1 = obj.collBox.posAndSize.c1 }; obj.collBox = obj.entity.NewCollBox(posAndSize, new float2(10f, 10f), obj.rotation.ToEulerAnglesZ()); // cObject.tr.position = new Vector3(newPos.x, newPos.y, 0); #if UNITY_EDITOR DebugDrowBox(obj.collBox, Color.blue, Time.deltaTime); #endif } }
- Actors + Jobs + Burst
Calculation and movement in Jobs from Unity.Jobs 0.1.1, Unity.Burst 1.1.2 libraries.
Safety Checks - off
Editor Attaching - off
JobsDebbuger - off
For normal operation of IJobParallelForTransform, all movable objects have a “parent object” (up to 255 pieces of objects in each “parent” according to the recommendation for maximum performance).
Update codepublic void Tick(float delta) { if (index <= 0) return; handlePositionUpdate.Complete(); #if UNITY_EDITOR for (var i = 0; i < index; i++) { var obj = nObj[i]; DebugDrowBox(obj.collBox, Color.blue, Time.deltaTime); } #endif jobPositionUpdate.nSetMove = nSetMove; jobPositionUpdate.nObj = nObj; jobPositionUpdate.deltaTime = delta; handlePositionUpdate = jobPositionUpdate.Schedule(transformsAccessArray); } } [BurstCompile] struct JobPositionUpdate : IJobParallelForTransform { public NativeArray<SetMove> nSetMove; public NativeArray<Obj> nObj; [Unity.Collections.ReadOnly] public float deltaTime; public void Execute(int index, TransformAccess transform) { var setMove = nSetMove[index]; var velocityToOneFrame = nSetMove[index].velocityToOneSecond * deltaTime; // setMove.observedDistance += velocityToOneFrame; var t = setMove.observedDistance / setMove.distanceFull; if (t > 1f) t = 1f; var newPos = t.CalculateBesierPos(setMove.posToMove.c0, setMove.posToMove.c2,setMove.posToMove.c1); nSetMove[index] = setMove; // var obj = nObj[index]; obj.properties.c0 = newPos; var posAndSize = new float2x2 { c0 = newPos, c1 = obj.collBox.posAndSize.c1 }; obj.collBox = obj.entity.NewCollBox(posAndSize, new float2(10f, 10f), obj.rotation.ToEulerAnglesZ()); nObj[index] = obj; // transform.position = (Vector2) newPos; } } public struct SetMove { public float2x3 posToMove; public float distanceFull; public float velocityToOneSecond; public float observedDistance; }
- Actors + Parallel.For
Instead of the usual For loop over a group of moving entities, Parallel.For is used from the System.Threading.Tasks library. It calculates the new position and the collider in parallel flows. Moving an object is carried out in a neighboring group.
Update codepublic void Tick(float delta) { Parallel.For(0, groupMoveBezier.length, i => { ref var entity = ref groupMoveBezier[i]; var cMoveBezier = entity.ComponentMoveBezier_actorsParallel(); ref var obj = ref entity.ComponentObject().obj; // var velocityToOneFrame = cMoveBezier.velocityToOneSecond * delta; cMoveBezier.observedDistance += velocityToOneFrame; var t = cMoveBezier.observedDistance / cMoveBezier.distanceFull; if (t > 1f) t = 1f; var newPos = t.CalculateBesierPos(cMoveBezier.posToMove.c0, cMoveBezier.posToMove.c2,cMoveBezier.posToMove.c1); // obj.properties.c0 = newPos; var posAndSize = new float2x2 { c0 = newPos, c1 = obj.collBox1.posAndSize.c1 }; obj.collBox1 = obj.entity.NewCollBox(posAndSize, new float2(10f, 10f), obj.rotation.ToEulerAnglesZ()); }); // foreach (ent entity1 in groupMoveBezier) { var cObject = entity1.ComponentObject(); cObject.tr.position = new Vector3(cObject.obj.properties.c0.x, cObject.obj.properties.c0.y, 0); #if UNITY_EDITOR DebugDrowBox(cObject.obj.collBox1, Color.blue, Time.deltaTime); #endif } }
Testing with moving [1]:
500 objects
(a picture from the editor near the text with FPS to show what is visually happening there)
- MonoBehaviour sequential:
- Actors sequential:
- Actors + Jobs + Burst:
- Actors + Parallel.For:
5000 objects
- MonoBehaviour sequential:
- Actors sequential:
- Actors + Jobs + Burst:
- Actors + Parallel.For:
50,000 objects
- MonoBehaviour sequential:
- Actors sequential:
- Actors + Jobs + Burst:
- Actors + Parallel.For:
Actors + Threaded (built in Actors parallelization on System.Threading)
Actors has the ability to hold all components of the game in structures instead of classes. This is hemorrhoids in terms of writing code, but under such conditions, the program works more with the stack, rather than with the managed heap, which significantly affects its speed.
Update code
public void Tick(float delta) { groupMoveBezier.Execute(delta); for (int i = 0; i < groupMoveBezier.length; i++) { ref var cObject = ref groupMoveBezier.entities[i].ComponentObject(); cObject.tr.position = new Vector3(cObject.obj.properties.c0.x, cObject.obj.properties.c0.y, 0); #if UNITY_EDITOR DebugDrowBox(cObject.obj.collBox, Color.blue, Time.deltaTime); #endif } } static void HandleCalculation(SegmentGroup segment) { for (int i = segment.indexFrom; i < segment.indexTo; i++) { ref var entity = ref segment.source.entities[i]; ref var cMoveBezier = ref entity.ComponentMoveBezier(); ref var cObject = ref entity.ComponentObject(); ref var obj = ref cObject.obj; // var velocityToOneFrame = cMoveBezier.velocityToOneSecond * segment.delta; cMoveBezier.observedDistance += velocityToOneFrame; var t = cMoveBezier.observedDistance / cMoveBezier.distanceFull; if (t > 1f) t = 1f; var newPos = t.CalculateBesierPos(cMoveBezier.posToMove.c0, cMoveBezier.posToMove.c2, cMoveBezier.posToMove.c1); // obj.properties.c0 = newPos; var posAndSize = new float2x2 { c0 = newPos, c1 = obj.collBox.posAndSize.c1 }; obj.collBox = obj.entity.NewCollBox(posAndSize, new float2(10f, 10f), obj.rotation.ToEulerAnglesZ()); } }
on class components
on structure components
In this case, we get + 10% to FPS, but in the example there are only two component-structures, and not tens, as it should be in the final product. Non-linear growth of FPS is possible here as the components of the reference types program are replaced by value types .
Conclusion
- In all cases, the FPS in Actors without Parallel.For increases by about two times, and with it - by three times compared with MonoBehaviour sequential. With the increase in mathematical calculations, these proportions remain.
- For me, an additional advantage of ECS Actors over MonoBehaviour sequential is that parallelization of calculations, adding to the speed, is added elementarily.
- Using Actors + Jobs + Burst increases FPS by about ten times compared to MonoBehaviour sequential
- Admittedly, such an increase in FPS is largely due to Burst. Of course, for its normal operation, you need to use data types from Unity.Mathematics (for example, replace Vector3 with float3)
And it’s very important: on my processor with 50,000 objects on the screen to raise FPS with before !
The following points must be observed:
1) If in the calculations you can do without a library, then it is better not to use it (red marker - bad, green - good)
2) You cannot use the Mathf library - only math, otherwise burst will not be able to vectorize and process the data.
- Judging by several third-party tests, MonoBehavior sequential with 50,000 objects shows the same ~ 50fps everywhere. But working on Actors + Jobs or Threaded is very different.
Also, the more modern the processor, the more useful it is to break up the work into several “queued” Jobs: position calculation, collider, moving to a position.
You can download a test program and compare the work of Actors + Jobs + Burst [one Job] with Actors + Jobs + Burst [four Job]. (On my four-core processor without hyper trading, the first test is -0.2ms faster with 50,000 objects) - The effectiveness of ECS depends on the number of additional elements (render, Unity physics, etc.).
[1] I do not know what the performance is in other ECS frameworks, in ECS-Unity / DOTS systems.
Test source
Thanks to Oleg Morozov (BenjaminMoore) for editing on jobs, adding SceneSelector and a new fps counter.
Thanks to iurii zakipnyi for the instructions, revisions, and the additional test Actors + Jobs + Burst [four Job]