Unity-Detailed Exploration of ECS High Performance

Posted by halojoy on Tue, 10 Sep 2019 05:19:43 +0200

Links to the original text: https://www.yxkfw.com/thread-58727-1-1.html

Preface
This article is the first one.< Ten-minute Unity ECS > The sequel focuses on the characteristics of efficiency and performance. Whether you've been exposed to Unity's ECS, you want to get a deeper understanding of it, or you haven't, but you want to understand some of the principles of this part of high performance, it doesn't prevent you from reading this article.
As in the previous section, this paper will focus on a small Demo, and compare the ECS mode with the old version in programming ideas and coding characteristics and reasons.
Demo content
Ball shower - 2 W balls fall freely, then return to the top after falling, so continuous circulation, like rain. It's probably like this:

Traditional practices
Write a loop and generate 2 W GameObject s. Add MeshFilter, MeshRenderer and a freefall component to each object. The code is as follows:
Free falling body assembly

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;

    public class LagacyDrop : MonoBehaviour
    {
        public float mass;
        public float delay;
        public float velocity;

        void Update()
        {
            if (this.delay > 0)
            {
                this.delay -= Time.deltaTime;
            }
            else
            {
                Vector3 pos = transform.position;
                float v = this.velocity + GravitySystem.G * this.mass * Time.deltaTime;
                pos.y += v;
                if (pos.y < GravitySystem.bottomY)
                {
                    pos.y = GravitySystem.topY;
                    this.velocity = 0f;
                    this.delay = Random.Range(0, 10f);
                }
                transform.position = pos;
            }
        }
    }

 

Generate code fragments of objects (spawnCount = 20000)

    void StartLagacyMethod()
    {
            var rootGo = new GameObject("Balls");
            rootGo.transform.position = Vector3.zero;

            for (int i = 0; i < spawnCount; ++i)
            {
                    var go = new GameObject();
                    var meshFilter = go.AddComponent<MeshFilter>();
                    meshFilter.sharedMesh = mesh;

                    var meshRd = go.AddComponent<MeshRenderer>();
                    meshRd.sharedMaterial = mat;

                    var dropComponent = go.AddComponent<LagacyDrop>();
                    dropComponent.delay = 0.02f * i;
                    dropComponent.mass = Random.Range(0.5f, 3f);

                    Vector3 pos = UnityEngine.Random.insideUnitSphere * 40;
                    go.transform.parent = rootGo.transform;
                    pos.y = GravitySystem.topY;
                    go.transform.position = pos;
            }
    }

 

Operation results:

PC configuration is Ryzen 5 1600 + 16G DDR4 + GTX1070, only 30 frames, and generally only 20+
ECS approach

  • Realizing Free Falling Component and Free Falling System
  • Create entities with batch interfaces, add and set free-fall components, location components, and Mesh renderers to entities, where location components and Mesh renderers are built-in components of Unity's ECS.


Combined with code explanation:
Free falling body assembly

    using Unity.Entities;

    [System.Serializable]
    public struct GravityComponentData : IComponentData
    {
        public float mass;
        public float delay;
        public float velocity;
    }

    public class GravityComponent : ComponentDataWrapper<GravityComponentData> { }

 

  • Component Data Wrapper/Shared Component Data Wrapper: The corresponding component is a wrapper of IComponentData/IShared Component Data. Purpose: Wrap a struct as MonoBehavior and add it to prefab. Principle: Wrapper's base class inherits from MonoBehavior and has a generic T member, the component to be wrapped.
  • Note that the class name should be the same as the file name, otherwise you can't add it when Editor edits prefab.


Free-falling body system

    using Unity.Entities;
    using Unity.Transforms;
    using UnityEngine;

    public class GravitySystem : ComponentSystem
    {
        struct Filter
        {
            public readonly int Length;
            public ComponentDataArray<GravityComponentData> gravity;
            public ComponentDataArray<Position> position;
        }

        [Inject] Filter data;
        public static float G = -20f;
        public static float topY = 20f;
        public static float bottomY = -100f;
        protected override void OnUpdate()
        {
            for (int i = 0; i < data.Length; ++i)
            {
                var gravityData = data.gravity[i];
                if (gravityData.delay > 0)
                {
                    gravityData.delay -= Time.deltaTime;
                    data.gravity[i] = gravityData;
                }
                else
                {
                    Vector3 pos = data.position[i].Value;
                    float v = gravityData.velocity + G * gravityData.mass * Time.deltaTime;
                    pos.y += v;
                    if (pos.y < bottomY)
                    {
                        pos.y = topY;
                        gravityData.velocity = 0f;
                        gravityData.delay = Random.Range(0, 10f);
                        data.gravity[i] = gravityData;
                    }
                    data.position[i] = new Position() { Value = pos };
                }
                
            }
        }
    }

 

Test class

    using Unity.Collections;
    using Unity.Entities;
    using Unity.Rendering;
    using Unity.Transforms;
    using UnityEngine;

    public class BallDropMain : MonoBehaviour
    {
        public Material mat;
        public Mesh mesh;
        public GameObject ballPrefab;

        public int spawnCount = 5000;

        void Start()
        {
            //StartLagacyMethod();
            StartECSMethod();
            //StartECSMethod(ballPrefab);
        }

        void StartECSMethod(GameObject prefab = null)
        {
            var entityMgr = World.Active.GetOrCreateManager<EntityManager>();
            var entities = new NativeArray<Entity>(spawnCount, Allocator.Temp);

            //Two ways of creating entities
            if (prefab)
            {
                entityMgr.Instantiate(prefab, entities);
            }
            else
            {
                var archeType = entityMgr.CreateArchetype(typeof(GravityComponentData), typeof(Position), typeof(MeshInstanceRenderer));
                entityMgr.CreateEntity(archeType, entities);
            }

            var meshRenderer = new MeshInstanceRenderer()
            {
                mesh = mesh,
                material = mat,
            };
            //Add Components
            for (int i = 0; i < entities.Length; ++i)
            {
                Vector3 pos = UnityEngine.Random.insideUnitSphere * 40;
                pos.y = GravitySystem.topY;
                var entity = entities[i];
                entityMgr.SetComponentData(entity, new Position { Value = pos });
                entityMgr.SetComponentData(entity, new GravityComponentData { mass = Random.Range(0.5f, 3f), delay = 0.02f * i });
                entityMgr.SetSharedComponentData(entity, meshRenderer);
            }

            entities.Dispose();
        }

        void StartLagacyMethod()
        {
            var rootGo = new GameObject("Balls");
            rootGo.transform.position = Vector3.zero;

            for (int i = 0; i < spawnCount; ++i)
            {
                var go = new GameObject();
                var meshFilter = go.AddComponent<MeshFilter>();
                meshFilter.sharedMesh = mesh;

                var meshRd = go.AddComponent<MeshRenderer>();
                meshRd.sharedMaterial = mat;

                var dropComponent = go.AddComponent<LagacyDrop>();
                dropComponent.delay = 0.02f * i;
                dropComponent.mass = Random.Range(0.5f, 3f);

                Vector3 pos = UnityEngine.Random.insideUnitSphere * 40;
                go.transform.parent = rootGo.transform;
                pos.y = GravitySystem.topY;
                go.transform.position = pos;
            }
        }
    }

 

ativeArray: Look at the name should be an array provided by the C++ layer. Officially, it's more efficient, and it's actually used. But because it won't be used by GC, you must be very careful to use it. When you run out of it, you should take the initiative to Dispose it. Archetype: Perhaps translated as "prototype", it can be understood as the "template" of an entity, which means that it contains multiple types and is the description of the data structure of an entity. Then when creating entities and allocating memory for entities, the appropriate size of memory will be allocated to create entities in a way: generally, two interfaces, EntityManager.CreateEntity and EntityManager.Instantiate, both support Native Array initialization, InstantiatePosition and MesshInstanceRenderer: Unity are built-in for ECS. Several lightweight class libraries allow entities to be separated from GameObject, such as location, rotation, grid and renderer, but without transform, rigidbody about [Inject]: Inject must be injected into struct, and struct can be seen as a filter to filter out the entity components you care about. Note that although there are multiple arrays in a struct, the length of each array is the same, and the components with the same subscript of each array actually belong to the same entity, so you can declare a readonly int member (of course, you can also use. Length of the array) and put the same length separately.

Hang the object of the startup script

prefab of the ball

Operation results

Therefore, the comparison shows that without GPU Instancing, the performance hotspot is that the CPU submits DrawCall, so the frame rate is the same without ECS. After opening, the performance hotspot shifts to the calculation of spherical motion, Demo using ECS shows its advantages.
Understanding ECS Framework

  • ArcheType and Chunk

ArcheType represents the type of entity, and the memory block allocated when creating an entity is called chunk. Formally speaking, chunk belongs to ArcheType. This concept is very important. Please remember it.
For example, there is currently an entity, ArcheType is composed of two component types A and B, and this entity is currently associated with a chunk. When component C is added to an entity, a new ArcheType is generated and a chunk is reassigned to associate with the new ArcheType. Also remember that this is closely related to the efficiency improvement described below.

  • Sequential memory layout improves CPU access efficiency

We should all know this more or less. First, the CPU has a first-level and second-level 3J cache. The nearer to the CPU, the faster the access speed is. First, we ignore the details of 123J and the elimination strategy and so on. It is unified as a cache. When accessing a certain segment of memory, the adjacent memory will also be loaded into the cache by way of optimization strategy. So if the next CPU instruction hits the data in the cache, it doesn't need to go back to memory to fetch the data. The speed increase is huge.
Based on the above principles, the memory layout of ECS is designed sequentially to optimize for CPU cache hits. For example, in the example above, we created 2 W entities, and the memory layout looks like this:


The combined test results of GPU Instancing and ECS on my PC
So when traversing ComponentDataArray, all memory access hits the cache.
The official explanation for Component Data Array is actually an inert iterator that points directly to the address where the chunk is stored in memory and calculates the offset of the corresponding component by subscribing. But the authorities did not specify how to handle or optimize access speed if the created entities had subsets or intersections of their ArcheType s.

  • Data and Interface Separation, Data and Aspect Oriented Programming

The disadvantage of Object-Oriented: Inheritance is one of the three main characteristics of object-oriented programming (abstraction, inheritance, polymorphism), but also a major disadvantage of object-oriented. It leads us to inherit some useless data, waste memory and low memory hit rate. Moreover, because object-oriented design can not achieve perfect class hierarchy division in most cases, even if the design is satisfied for a time, it will be "attacked" by software iteration time after time, and become cumbersome. Therefore, the ECS framework development team believes that we should abandon the past object-oriented programming and use data-oriented programming for continuous and compact memory layout in order to improve memory hit rate.
The idea of ECS framework: Pay attention to data type, operation and state of the system, do not pay attention to the details of a specific object. Therefore, ECS is not omnipotent, the application of the premise is that your focus is biased towards the macro, and need to deal with a large number of similar objects. If you focus on detail, and the interaction between details is very delicate and complex, without a large number of similar objects, then object-oriented may be more appropriate.

As for the source code of ECS, as long as you import the package of ECS in your project, you can find it from the following directory:
%AppData%\..\Local\Unity\cache\packages\http://packages.unity.com

Topics: Unity Programming less