这篇文章会让你真正的认识到JobSystem的强大之处,并且还会感受到DOTS的恐怖之处!
那么下面我们通过实践测试来见证一下。
Job System (作业系统) 可以理解为多线程管理系统
我们通过Job System就可以编写与Unity其他部件交互的多线程代码,同时让编写正确的多线程代码变得更容易。
编写多线程代码可以提供更好的性能表现。这包括极大的提升提升和手机上更久的续航。
Job System的一个非常关键的方面是它可以融入Unity内部的原生Job System。这使得用户的代码可以和Unity共享worker threads。这种合作避免了创建更多线程,因为这可能会造成对于CPU资源的争抢。
在Job System中我们会使用到一种新的类型NativeContainer。它是一种托管值类型,为本机内存提供了一个相对安全的 C# 封装器,它包含一个指向非托管分配的指针。
与 Job System一起使用时,NativeContainer允许Job访问与主线程共享的数据,而不是拷贝数据。如果是拷贝数据会导致同样的数据到不同的Job中,其结果是相互隔离的,因此我们需要将结果存储在共享内存中,也就是NativeContainer。
Unity 附带了一个名为NativeArray的NativeContainer,用来代替传统的数组(T[])。
ECS为其进行了拓展Unity.Collections命名空间以包含其他类型的 NativeContainer:
- NativeList - 可调整大小的 NativeArray,类似于List
- NativeHashMap
- 键/值对,类似于Dictionary - NativeMultiHashMap
- 每个键有多个值。 - 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的时候自动释放
一个简单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进行计算所需的耗时。
计算方案是在每帧执行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");
}
///
/// 默认计算
///
public void NormalCalculation()
{
for (int i = 0; i < 100000; i++)
{
math.pow(math.sqrt(i), i);
}
}
从下图我们可以看到 ,在不使用JobSystem进行正常计算的情况下,我们每帧计算需耗时236毫秒左右,也就是0.2几秒,这种情况下我们的帧率直接跌破到4帧。可以说这种情况下运行我们的游戏已经毫无游戏体验了。
那么到这里肯定就有人会问:那我们没有办法去提升计算效率吗?
答案是 当然有!
那么接下来咱们一起来看下开启JobSystem后,性能会提升多少
这里的话我们已经开启了JobSystem,从下图我们可以明显的看到性能有很大的提升。
首先是我们的计算耗时,直接从236毫秒降到了37毫秒,计算效率整整提升了7倍,强大吧!。
其次是我们帧率,从原本的4FPS直接飙到了26FPS,提升了6倍不止。
当然看我们的CPU耗时也可以明显对比出两者之间的差距,这下应该体会到JobSystem的强大之处了吧。
当然,具体提升的性能,跟设备的处理器也有很大的关系,因为上篇博文也简单的介绍了JobSystem是什么,其实就是安全、易用的多线程系统,所以设备的支持的线程数越高,提升的性能就越高。
这里博主测试时使用的设备CPU是 i7-9750H 6核12线程
通过下图我们可以看到,在不开启JobSystem的情况下,我们的计算只在主线程种跑,且耗时高达236毫秒每帧,因此我们的性能各方面极具下降,并且我们的子线程全部都在闲着,并没有进行任何的工作。
通过下图我们明显可以看出开启JobSystem后的变化。
首先是我们的每帧耗时直降到37毫秒。
其次就是我们的Job开始了工作,他启动了多个线程去帮助我们的主线程进行运算,这使得的们的主线程压力骤减,所以比之前的计算直接提升了将近200毫秒。
并不是我们的主线程效率提高了,而是主线程有了帮手,让我们能在有限的时间内能够更好的去完成需要完成的计算。
其实就可以拿作我们开发团队的模式进行举例,首先是一个主程要去完成一个项目,可能会需要半年至一年不等,那么这时候我们最有效的缩短开发周期的方式就是,招人。招到一个人就相当于开辟一个子线程,那么多个人同时开发,效率自然就上去了,周期自然就缩短了。
当然他的作用不止这些,这里只是做个测试,具体要拿来做什么要看自己的需求于想法。
看完上面的性能与工作对比,你可能会感慨,JobSystem是真的强大啊,完全不用我们去做复杂的多线程管理,直接就把我们的性能硬生生提升了这么多。
是的,不可否认, 确实强大!
但是,你以为这就是极限了吗?
错了!
下面就让咱们一起看看什么才是极限!
为了达到极限值,咱们这里要使用到本来是在下一篇博客进行讲解的 Burst
要想了解Burst是什么 请移步 Unity 革命性技术DOST ECS入门一 Burst代码编译器
使用这个Burst我们只需要在我们的Job计算上添加 [BurstCompile]标志,并且在运行前在Eidtor下把Job/Enable Compilation打开即可
[BurstCompile]
///
///Job计算
///
[BurstCompile]
public struct JobCalculationStruct : IJob
{
public void Execute()
{
for (int i = 0; i < 100000; i++)
{
math.pow(math.sqrt(i), i);
}
}
}
下面我们开启Burst进行一下测试,看一下我们的性能能提升多少
在开启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");
}
///
/// 默认计算
///
public void NormalCalculation()
{
for (int i = 0; i < 100000; i++)
{
math.pow(math.sqrt(i), i);
}
}
///
/// 开始job计算
///
///
public JobHandle StartJobCalculation()
{
JobCalculationStruct jobCalculation = new JobCalculationStruct();
return jobCalculation.Schedule();
}
}
///
///Job计算
///
[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。