Unity——浅谈Unity ECS结合Job System详解

前言
本文是上一篇Unity ECS 高性能探索的续篇。在上一篇,我们用ECS实现了PC下同屏2w个球体,编辑器下跑到了120FPS。而在本篇,我们把性能继续解放,加上Job System,把同屏推进到8w个球体,60FPS。

性能瓶颈我们先来看一下上一篇实现的ECS同屏2w个球体,如果加到8w个会怎么样

只有不到40FPS,上一篇里我们提到了,开启GPU Instancing以后,性能热点从提交Draw Call转移到了物理运算。现在从2w个球体提高到8w个,性能热点又转移到哪了呢?如果我们想要更多怎么办?
首先我们分析下,37FPS * 每帧for循环处理8w个物体,也就是即使每次for循环里只做一次加法,每秒也接近300w次加法。更何况在Demo里,for循环里做的还是物理运算还有随机数运算。我们先来看看当前环境下的极限运算能力:

    void Start()
        {
            float t1 = Time.realtimeSinceStartup;
            float tmp = 0f;
            int cnt = 10000000;
            for (int i = 0; i < cnt; ++i)
            {
                tmp += Random.Range(-10f, 10f);
            }

            Debug.Log("Calculation per second:" + (int)(cnt / (Time.realtimeSinceStartup - t1)));
        }

 


大约是每秒做3700w次加法和随机数
而上一篇的Demo里,最坏情况下,for循环里要执行2次加法,2次乘法,1次随机数。再加上数组索引、变量赋值、getter/setter的调用和new的开销,而且还有渲染和维护8w个对象的基本引擎开销。我们可以大致推断出:37FPS已经逼近当前环境下单核的运算极限了
当然,我们可以通过一些代码优化的手段减少for循环里的运算以提升一点效率,但很明显提升不会太大。在单核已经接近极限的情况下,考虑的解决方案就只能是多线程,把运算转移到别的核里去。
在没有Job System以前,由于受限于Unity引擎的对象只能在主线程里访问,我们一般的做法是:抽象出自定义的数据结构,放到子线程里运算,主线程每帧取子线程的运算结果,应用到相应的对象上。
Job System解决的是什么问题如上所述,我们如果遇到卡主线程的逻辑,一般考虑丢给子线程解决。但是在游戏开发中,我们常常遇到的一般是“碎片化”的任务逻辑,这些任务相互独立,来得快消失得也快。比如屏幕上的子弹,子弹的物理运动可以单独运算,如果每颗子弹都开启一个线程,这样线程开启和销毁的开销也太大。那也许你说搞个线程池重用吧,线程数太多的话上下文切换的开销也是很可观的,更何况怎么知道开多少个线程合适呢?
我们知道最理想的状态下,每个核绑定一个线程是最好的,但由于平台和硬件的差异使得实现比较复杂,因此我们需要一个帮助我们屏蔽平台和硬件差异的多线程的设计,这就是Job System。
Job System原本是Unity引擎的内部模块,后面多线程的需求多了,就干脆封装了一下提供给大家使用了。它屏蔽了直接对线程的操作,而是把异步逻辑封装成一个个“Job”,由引擎来调度和分配给合适的线程去执行,由引擎来控制线程与CPU核数的对应关系,达到最优的运行效率。
最简单的一个Job转自Unity手册:

    // Job adding two floating point values together
    public struct MyJob : IJob
    {
        public float a;
        public float b;
        public NativeArray<float> result;

        public void Execute()
        {
            result[0] = a + b;
        }
    }

 

Job是一个struct,并且需要实现IJob接口

    // Create a native array of a single float to store the result. This example waits for the job to complete for illustration purposes
    NativeArray<float> result = new NativeArray<float>(1, Allocator.TempJob);

    // Set up the job data
    MyJob jobData = new MyJob();
    jobData.a = 10;
    jobData.b = 10;
    jobData.result = result;

    // Schedule the job
    JobHandle handle = jobData.Schedule();

    // Wait for the job to complete
    handle.Complete();

    // All copies of the NativeArray point to the same memory, you can access the result in "your" copy of the NativeArray
    float aPlusB = result[0];

    // Free the memory allocated by the result array
    result.Dispose();

 

新建一个Job,调用Schedule()后即把任务放入等待队列里等候调度,同时返回一个handle,可以选择等待Job完成也可以选择继续干别的事情。
需要注意的是Job的成员类型必须是“Blittable Types”或者NativeContainer,Blittable Type的意思就是托管与非托管代码间交互不需要转换的类型,比如float属于System.Single类型,C#和C++对float的定义是一致的,不需要转换。但是bool属于System.Boolean,C#和C++的定义是不一致的,C++只认0/1,所以需要转换。哪些类型属于Blittable如果不清楚的可以查询这里。至于这个限制,是为了防止多线程冲突,Blittable类型的数据都可以直接拷贝而不会有并发访问的问题。NativeContainer是Unity底层提供的容器,Unity对这些容器做了处理,可以直接memcpy拷贝,所以也能够使用。
另外除了IJob以外,还有IJobParallelFor,用来在一个Job里处理批量数据的

    struct IncrementByDeltaTimeJob: IJobParallelFor
    {
        public NativeArray<float> values;
        public float deltaTime;

        public void Execute (int index)
        {
            float temp = values[index];
            temp += deltaTime;
            values[index] = temp;
        }
    }

 

还有IJobParallelForTransform,顾名思义就是特意做给Transform用的
ECS+Job System在ECS的结构中,xxxSystem是一个每帧在OnUpdate里执行逻辑的类,当OnUpdate的逻辑太多,我们就可以考虑把这部分逻辑封装成一个Job,丢到另外的线程去执行。针对ECS,Unity提供了一个JobComponentSystem实现多线程的系统。
这就是Demo修改过后的GravitySystem

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

    public class GravityJobSystem : JobComponentSystem
    {
        public static float G = -20;
        public static float topY = 20f;
        public static float bottomY = -100f;

        struct GravityJob : IJobProcessComponentData<GravityComponentData, Position>
        {
            //运行在其它线程中,需要拷贝一份GravityJobSystem的数据
            public float G;
            public float topY;
            public float bottomY;
            //非主线程不能使用Time.deltaTime
            public float deltaTime;
            //非主线程不能使用UnityEngine.Random
            public Unity.Mathematics.Random random;

            //物理运算
            public void Execute(ref GravityComponentData gravityData, ref Position positionData)
            {
                if (gravityData.delay > 0)
                {
                    gravityData.delay -= deltaTime;
                }
                else
                {
                    Vector3 pos = positionData.Value;
                    float v = gravityData.velocity + G * gravityData.mass * deltaTime;
                    pos.y += v;
                    if (pos.y < bottomY)
                    {
                        pos.y = topY;
                        gravityData.velocity = 0f;
                        gravityData.delay = random.NextFloat(0, 10);
                    }
                    positionData.Value = pos;
                }
            }
        }

        //每帧生成任务丢给Job System
        protected override JobHandle OnUpdate(JobHandle inputDeps)
        {
            GravityJob job = new GravityJob()
            {
                G = G,
                topY = topY,
                bottomY = bottomY,
                deltaTime = Time.deltaTime,
                random = new Unity.Mathematics.Random((uint)(Time.time * 1000 + 1)),
            };
            

            return job.Schedule(this, inputDeps);
        }
    }

 


IJobProcessComponentData提供了IComponentData对应的IJob封装,只要是实现这个接口的Job,引擎层会自动实现[Inject]的功能,根据泛型筛选出对应的组件注入,并且一个个传入调用Execute。值得注意的是,IJobProcessComponentData只有T0~T3四种泛型,也许Unity的意思是如果需要注入超过4个组件的,这个系统的设计就不太合理了。
优化后的结果:

8w个球可以跑在60~70FPS之间,我们还可以尝试下推进到10w个球球

看起来10w个差不多到极限了

更多的优化:

  • 尝试过启用Burst编译,然而并没有看到更好的效果,不知是用得不对还是瓶颈不在这
  • 任务管理器里看到Unity启动后所有核都同时上升,可以看出Job System把一个Job也均衡分布到每个核上了,但每个核都只占50%,暂且未知是系统或者硬件限制还是Unity的优化不足,或者有什么方式可以继续往上面增加任务


总结

  • ECS帮助我们解决CPU对内存的访问效率,以及规避了面向对象中继承的弊端
  • Job System帮助我们用异步的方式解决单核运算量不足,以及多线程多核负载均衡的问题
发布了97 篇原创文章 · 获赞 21 · 访问量 28万+

猜你喜欢

转载自blog.csdn.net/sun124608666/article/details/100693224