Unity 革命性技术DOST入门三 JobSystem系统


前言

这篇文章会让你真正的认识到JobSystem的强大之处,并且还会感受到DOTS的恐怖之处!

那么下面我们通过实践测试来见证一下。

1.什么是Job System

Job System (作业系统) 可以理解为多线程管理系统

我们通过Job System就可以编写与Unity其他部件交互的多线程代码,同时让编写正确的多线程代码变得更容易。

编写多线程代码可以提供更好的性能表现。这包括极大的提升提升和手机上更久的续航。

Job System的一个非常关键的方面是它可以融入Unity内部的原生Job System。这使得用户的代码可以和Unity共享worker threads。这种合作避免了创建更多线程,因为这可能会造成对于CPU资源的争抢。

3.概述

在Job System中我们会使用到一种新的类型NativeContainer。它是一种托管值类型,为本机内存提供了一个相对安全的 C# 封装器,它包含一个指向非托管分配的指针。

与 Job System一起使用时,NativeContainer允许Job访问与主线程共享的数据,而不是拷贝数据。如果是拷贝数据会导致同样的数据到不同的Job中,其结果是相互隔离的,因此我们需要将结果存储在共享内存中,也就是NativeContainer。

Unity 附带了一个名为NativeArray的NativeContainer,用来代替传统的数组(T[])。

ECS为其进行了拓展Unity.Collections命名空间以包含其他类型的 NativeContainer:

  • NativeList - 可调整大小的 NativeArray,类似于List
  • NativeHashMap<T, R> - 键/值对,类似于Dictionary<T, R>
  • NativeMultiHashMap<T, R> - 每个键有多个值。
  • NativeQueue - 先进先出队列,类似于Queue

创建 NativeContainer 时,必须指定所需的内存分配类型(Allocator),分配类型取决于Job运行的时间。设置不同的值以便在每种情况下获得最佳性能。

Allocator.Temp - 具有最快的分配速度。此类型适用于寿命为一帧或更短的分配。不应该使用 Temp 将 NativeContainer 分配传递给Job。在从方法调用返回之前,需要调用 Dispose 方法。

Allocator.TempJob - 的分配速度比 Temp 慢,但比 Persistent 快。此类型适用于寿命为四帧的分配,并具有线程安全性。如果没有在四帧内对其执行 Dispose 方法,控制台会输出警告。大多数逻辑量少的Job都使用这种类型。

Allocator.Persistent - 是最慢的分配,但可以在您所需的任意时间内持续存在,如果有必要,可以在整个应用程序的生命周期内存在。此分配器是直接调用 malloc 的封装器。持续时间较长的Job可以使用这种类型。在非常注重性能的情况下不应使用 Persistent。
例如:

NativeArray result = new NativeArray(10, Allocator.TempJob);

注:使用NativeContainer需要我们手动Dispose,而不是等GC的时候自动释放

3.JobSystem 使用介绍

一个简单job定义的例子

public struct MyJob : IJob
{
    public float a;
    public float b;
    public NativeArray<float> result;

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

在合适的时间调用Schedule将job放入到job的执行队列中。一旦job被调度,你不能中途打断一个job的执行。

注意:你只能从主线程中调用Schedule

    void Update()
    {
   		NativeArray<float> result = new NativeArray<float>(1, Allocator.TempJob);
		MyJob jobData = new MyJob();
		jobData.a = 10;
		jobData.b = 10;
		jobData.result = result;

		JobHandle handle = jobData.Schedule();
		handle.Complete();
		float aPlusB = result[0];
		result.Dispose();
    }

使用JobHandle来让你的代码在主线程等待直到你的job执行完毕。为了做到这样,需要在JobHandle上调用Complete方法。这样的话,你就确定主线程可以安全访问job之前使用的NativeContainer。

调用 JobHandle.Complete() 则表示你已经完成作业,让主线程不用等你了,可以开始访问你处理过的数据了。

JobSystem性能介绍

首先我们需要通过一个高频率且复杂的计算来验证在一帧内正常计算和使用JobSystem进行计算所需的耗时。

计算方案是在每帧执行10个 10万次的 次方计算和开平方计算

这里的用到的 math 是DOTS配套的Mathematics数学库

测试源码我会放到文章末尾

下面是数学计算代码:

    void Update()
    {
        //记录一下开始的时间
        float startTime = Time.realtimeSinceStartup;
        for (int i = 0; i < 10; i++)
        {
            NormalCalculation();
       }
        //打印计算的耗时  这个耗时是毫数
        Debug.Log((Time.realtimeSinceStartup - startTime) * 1000 + "ms");
    }
    /// <summary>
    /// 默认计算
    /// </summary>
    public void NormalCalculation()
    {
        for (int i = 0; i < 100000; i++)
        {
            math.pow(math.sqrt(i), i);
        }
    }

1.正常计算(不使用JobSystem)

从下图我们可以看到 ,在不使用JobSystem进行正常计算的情况下,我们每帧计算需耗时236毫秒左右,也就是0.2几秒,这种情况下我们的帧率直接跌破到4帧。可以说这种情况下运行我们的游戏已经毫无游戏体验了。

那么到这里肯定就有人会问:那我们没有办法去提升计算效率吗?

答案是 当然有!

那么接下来咱们一起来看下开启JobSystem后,性能会提升多少
在这里插入图片描述

2.开启起JobSystem

这里的话我们已经开启了JobSystem,从下图我们可以明显的看到性能有很大的提升。

首先是我们的计算耗时,直接从236毫秒降到了37毫秒,计算效率整整提升了7倍,强大吧!。

其次是我们帧率,从原本的4FPS直接飙到了26FPS,提升了6倍不止。

当然看我们的CPU耗时也可以明显对比出两者之间的差距,这下应该体会到JobSystem的强大之处了吧。

当然,具体提升的性能,跟设备的处理器也有很大的关系,因为上篇博文也简单的介绍了JobSystem是什么,其实就是安全、易用的多线程系统,所以设备的支持的线程数越高,提升的性能就越高。

这里博主测试时使用的设备CPU是 i7-9750H 6核12线程

接下来咱们看一下Profile性能
在这里插入图片描述

3.Profile性能对比 (未开启JobSystem)

通过下图我们可以看到,在不开启JobSystem的情况下,我们的计算只在主线程种跑,且耗时高达236毫秒每帧,因此我们的性能各方面极具下降,并且我们的子线程全部都在闲着,并没有进行任何的工作。
在这里插入图片描述

3.Profile性能对比 (开启JobSystem)

通过下图我们明显可以看出开启JobSystem后的变化。

首先是我们的每帧耗时直降到37毫秒。

其次就是我们的Job开始了工作,他启动了多个线程去帮助我们的主线程进行运算,这使得的们的主线程压力骤减,所以比之前的计算直接提升了将近200毫秒。

并不是我们的主线程效率提高了,而是主线程有了帮手,让我们能在有限的时间内能够更好的去完成需要完成的计算。

其实就可以拿作我们开发团队的模式进行举例,首先是一个主程要去完成一个项目,可能会需要半年至一年不等,那么这时候我们最有效的缩短开发周期的方式就是,招人。招到一个人就相当于开辟一个子线程,那么多个人同时开发,效率自然就上去了,周期自然就缩短了。

当然他的作用不止这些,这里只是做个测试,具体要拿来做什么要看自己的需求于想法。
在这里插入图片描述
看完上面的性能与工作对比,你可能会感慨,JobSystem是真的强大啊,完全不用我们去做复杂的多线程管理,直接就把我们的性能硬生生提升了这么多。

是的,不可否认, 确实强大!

但是,你以为这就是极限了吗?

错了!

下面就让咱们一起看看什么才是极限!

为了达到极限值,咱们这里要使用到本来是在下一篇博客进行讲解的 Burst

要想了解Burst是什么 请移步 Unity 革命性技术DOST ECS入门一 Burst代码编译器

使用这个Burst我们只需要在我们的Job计算上添加 [BurstCompile]标志,并且在运行前在Eidtor下把Job/Enable Compilation打开即可

[BurstCompile]

/// <summary>
///Job计算
/// </summary>
[BurstCompile]
public struct JobCalculationStruct : IJob
{
    public void Execute()
    {
        for (int i = 0; i < 100000; i++)
        {
            math.pow(math.sqrt(i), i);
        }
    }
}

下面我们开启Burst进行一下测试,看一下我们的性能能提升多少

4.极限性能

在这里插入图片描述
在开启Burst之后我们可以看到,我们的性能得到了质的飞跃!

我们的帧计算耗时狂降到了0.2-0.1毫秒不等,我们的FPS已经从之前的26FPS狂飙到了的1160FPS。

什么叫恐怖?

这就叫恐怖!

这。 我还能说什么… ? 真的什么都不用说,看图体会就完事了!

测试环境:2019.4.18

下面附上测试源码:

using UnityEngine;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Collections;
using Unity.Burst;
public class JobUseTest : MonoBehaviour
{
    //是否开启Job计算
    [SerializeField]
    private bool mIsJob;
    void Update()
    {
        //记录一下开始的时间
        float startTime = Time.realtimeSinceStartup;
        if (mIsJob)
        {
            NativeList<JobHandle> jobHandleList = new NativeList<JobHandle>(Allocator.Temp) ;
            for (int i = 0; i < 10; i++)
            {
                JobHandle jobHandle = StartJobCalculation();
                jobHandleList.Add(jobHandle);
            }
            //在job系统处理时 会暂停主线程,要等我们job系统的所有计算都结束了之后,继续跑主线程
            JobHandle.CompleteAll(jobHandleList);
            jobHandleList.Dispose();
        }
        else
        {
            for (int i = 0; i < 10; i++)
            {
                NormalCalculation();
            }
        }
        //打印计算的耗时  这个耗时是毫数
        Debug.Log((Time.realtimeSinceStartup - startTime) * 1000 + "ms");
    }
    /// <summary>
    /// 默认计算
    /// </summary>
    public void NormalCalculation()
    {
        for (int i = 0; i < 100000; i++)
        {
            math.pow(math.sqrt(i), i);
        }
    }
    /// <summary>
    /// 开始job计算
    /// </summary>
    /// <returns></returns>
    public JobHandle StartJobCalculation()
    {
        JobCalculationStruct jobCalculation = new JobCalculationStruct();
        return  jobCalculation.Schedule();
    }
}
/// <summary>
///Job计算
/// </summary>
[BurstCompile]
public struct JobCalculationStruct : IJob
{
    public void Execute()
    {
        for (int i = 0; i < 100000; i++)
        {
            math.pow(math.sqrt(i), i);
        }
    }
}

DOTS入门视屏教程

下一篇:Unity 革命性技术DOST入门四 Raycast射线检测

文章来自于铸梦老师,铸梦之路系列课程。
想了解更多框架、帧同步技术、UGUI优化相关技术可在企鹅kt搜索 铸梦xy。

  • 14
    点赞
  • 44
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

铸梦xy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值