https://qiita.com/mao_/items/e05cd355cdf78fd22593
官方文档: https://docs.unity3d.com/Manual/JobSystem.html
https://docs.unity3d.com/Manual/JobSystemTroubleshooting.html
C# Job System 提示和故障排除
使用Unity C#作业系统时,请确保遵守以下内容:
不要从作业访问静态数据
从作业访问静态数据会绕过所有安全系统。如果您访问错误的数据,您可能会以意想不到的方式崩溃Unity。例如,访问MonoBehaviour可能会导致域重新加载崩溃。
注意:由于存在这种风险,Unity的未来版本将阻止使用静态分析从作业进行全局变量访问。如果您确实访问作业中的静态数据,则应该期望您的代码在Unity的未来版本中中断。
Flush scheduled batches
如果希望作业开始执行,则可以使用JobHandle.ScheduleBatchedJobs刷新计划批处理。请注意,调用此方法会对性能产生负面影响。不刷新批处理会延迟调度,直到主线程等待结果。在所有其他情况下,使用JobHandle.Complete来启动执行过程。
注意:在实体组件系统(ECS)中,将为您隐式刷新批处理,因此JobHandle.ScheduleBatchedJobs不需要调用。
不要尝试更新NativeContainer内容
由于缺少ref返回,因此无法直接更改NativeContainer的内容。例如,nativeArray[0]++;与var temp = nativeArray[0]; temp++;不更新值的写入相同nativeArray。
相反,您必须将索引中的数据复制到本地临时副本,修改该副本并将其保存回来,如下所示:
MyStruct temp = myNativeArray[i];
temp.memberVariable = 0;
myNativeArray[i] = temp;
Call JobHandle.Complete以重新获得所有权
跟踪数据所有权需要在主线程再次使用它们之前完成依赖项。检查JobHandle.IsCompleted是不够的。您必须调用该方法JobHandle.Complete以重新获得NativeContainer主线程的类型的所有权。调用Complete还可以清除安全系统中的状态。不这样做会引入内存泄漏。如果您在每个帧中安排新作业,并且依赖于前一帧的作业,则此过程也适用。
在主线程中使用Schedule和Complete
你只能调用时间表和Complete主线程。如果一个作业依赖于另一个作业,则使用JobHandle管理依赖关系而不是尝试在作业内安排作业。
在合适的时间使用Schedule和Complete
Schedule只要您拥有所需的数据就立即打电话给工作,并且Complete在您需要结果之前不要打电话给它。优秀的做法是安排一个您不需要等待的工作,而不是与正在运行的任何其他工作竞争。例如,如果在一帧结束和下一帧的开始之间没有作业正在运行,并且可以接受一帧延迟,则可以将作业调度到帧的末尾并使用其结果在以下框架中。或者,如果您的游戏与其他工作的转换期间相比,并且框架中的其他位置存在大量未充分利用的时期,则更有效地安排您的工作。
将NativeContainer类型标记为只读
请记住,作业NativeContainer默认情况下对类型具有读写访问权限。[ReadOnly]适当时使用该属性可提高性能。
检查数据依赖性
在Unity Profiler窗口,主线程上的标记“WaitForJobGroup”表示Unity正在等待工作线程上的作业完成。此标记可能意味着您已在某处应引入数据依赖关系。寻找JobHandle.Complete跟踪数据依赖关系的位置,这些数据依赖关系迫使主线程等待。
调试jobs
作业具有一个Run函数,您可以使用它来代替Schedule在主线程上立即执行作业。您可以将其用于调试目的。
不要在作业中分配托管内存
在作业中分配托管内存非常慢,并且该作业无法使用Unity Burst编译器来提高性能。Burst是一种新的基于LLVM的后端编译器技术,可以让您更轻松。它需要C#作业并利用您平台的特定功能生成高度优化的机器代码。
以下是翻译文章: https://qiita.com/mao_/items/e05cd355cdf78fd22593
由于在使用JobSystem实现函数时积累了一些知识,因此写这篇文章,作为个人提示集合。
然而,仍有许多地方需要探索。有些地方这些信息不是最佳解决方案,所以如果可以作为一个具有“在这种情况下可能有效”的细微差别的例子,那将是幸运的。 。
作为要解释的主要内容,我将基于我之前实现的VRMSpringBone的JobSystem(&ECS)对应的内容进行推进。
( 此版本的VRMSpringBone与JobSystem兼容,缩写为“JobSpringBone”)
有关此项目和完整项目的文件可在以下网址获得:
顺便说一句,我不会在文章中解释JobSystem基础知识,不过文章最后的连接等可以参考。
(Schedule 会被翻译为 安排/调度 Job 翻译为 工作/作业)
Job Schedule提示
1、关于Job的单位Schedule
作为要采用的实现示例,我想基于JobSpringBone中实现的以下两种管理方法进行解释。
- 在将要传递给Job的数据放在模型单元中之后安排Job
- 集中管理要传递给所有Job模型的数据,然后安排Job
在将要传递给作业的数据放在模型单元中之后安排作业
以下是在将要传递到Job的数据放在模型单元中之后Schedule Job的操作。
由于该特征是模型单元中的管理,因此要管理的数据的数量和要调度的Job的数量随着模型的增加而增加。因此,印象是随着要管理的模型数量的增加,Schedule 上的负载变得更高。
*例如,如果它是JobSpringBone,它将通过总共3次Job处理,例如“complider计算,父旋转采集,物理操作”,因此如果要显示10个模型,“3(数量) x 10(模型数量)= 30(最终安排的次数)将导致总计30个Job被安排。
保存传递给Job的数据的方法总结如下。(引自幻灯片)这
是一个要作为传递给Job的数据进行管理的图像,例如每个模型的shake对象的Transform数组和物理操作中使用的值。
有关详细信息,请参阅以下代码
-
- 整个Job的管理类。(转到附表)
-
- 模型具有的数据的定义
负载测量结果
以下是Profiler结果:有关
执行环境和测量时的处理的详细信息,请参阅“ 此处 ”。
如果查看结果,可以看到WorkerThread的Job序列很稀疏。
时间表上的负载没有后面提到的模型的增加或减少那么重,但似乎计划中的负载是粗略的......但原因本身是未知的。。
(如果您按照此示例计划大量作业,则会卡住......?)
顺便说一下,关于模型的动态增加和减少的负荷,数据可以由模型单元收集,或者与集中管理传递给后面描述的Job的数据的方法相比,它被大大抑制。
该测量方法具有256个模型的大量同时显示,因此如果要管理的模型很少并且模型变化很多,则该管理方法可能是有效的。(因为我们还没有介绍它,我们需要验证它)
集中管理要传递给所有Job模型的数据,然后安排Job
传递给所有Job模型的数据是集中管理的,然后集中安排和处理Job。
该功能是,无论添加多少个模型,要传递给Job的数据数量和要调度的作业数量始终保持不变,因此可以保证计划中的负载可以保持较低。
→即使有100个模型或1000个模型,时间表总是固定的次数是3次。
保存传递给Job的数据的方法总结如下。(引自幻灯片)
它是一个图像,它管理摇动对象的所有对象的Transform数组以及在一个缓冲区(传递给Job的数据)中的物理操作中使用的值。
有关详细信息,请参阅以下代码
-
- 整个Job的管理类。
-
- 模型具有的数据的定义
负载测量结果
以下是Profiler结果:
关于测量时的执行环境和处理内容,参见上面的“ 这里 ”。
因此,Schedule的负载不是那么重,似乎在WorkerThread中填充的Jobs更好。
这是一种印象,可以高速处理处理速度。
然而,一个模型的动态增加和减少具有增加负荷的感觉。(两个或两个巨大的尖峰正在增长的地方是相应的地方)
通过设计如何在内部保持缓冲区(例如,查看NativeContiner本身以便于增加或减少它),可以在某种程度上消除它。缓冲区重建负载导致大的尖峰。
※关于模型的增加和减少...如果NativeArray管理的数据仍然...... 我想知道TransformAccessArray周围的管理是否应该是好的....
此负载基于这样的前提,即此时重新创建256个主体的缓冲区,但是根据同时显示的数量,它可能在允许的范围内。
摘要
我已经解释了Job的单位大致安排。
回顾一下,您可以总结如下。
- 最好让Job→集中管理要传递的数据,并在Schedule→Collective中进行处理。
-
- 但是,除非我使用名为ECS的架构,否则我觉得数据管理可能会很麻烦。[1]
- 根据数量,您可能不会错过重建缓冲区的负担。
- 对于每个模型的管理方法,有时候当显示少量模型并且存在许多交换时,这更有效。
它们似乎都有优点/缺点,因此根据您的要求设计/测量可能会更好,同时考虑到这种情况。
2、不依赖于彼此的工作JobHandle.CombineDependencies合并为一个。
可以使用名为“ JobHandle.CombineDependencies ”的API将不相互依赖的作业合并为一个。
如果你很好地使用它,你可能能够对WorkerThread有一个良好的感觉,同时减少不必要的工作等待。
首先,我CombineDependencies将首先解释不使用的模式,然后CombineDependencies我将解释已按顺序优化的模式。
不使用CombineDependencies时
但在JobSpringBone中,我们将通过以下3个作业计算出震动的东西。
- Collider计算(UpdateColliderHashJob)
- 得到父的Rotation(UpdateParentRotationJob)
- 物理演算(LogicJob)
作为计算顺序,首先执行“计算Collider”和“获得父对象的旋转”,并且最终使用这两个点的计算结果执行物理计算。
如果放弃Job的依赖关系,它将是“计算父对象的旋转/获取→物理操作”,如果只是在代码中实现,则应通过控制JobHandle来设置依赖关系,如下所示。你可以
CentralizedJobScheduler.cs
// MonoBehaviour.LateUpdate()调用
void ExecuteJobs()
{
// 缓冲区初始化等(处理省略)
.........................
// Collider更新
var handle = new UpdateColliderHashJob
{
GroupParams = this._colliderGroupJobData.GroupParams,
ColliderHashMap = this._colliderHashMap.ToConcurrent(),
}.Schedule(this._colliderGroupJobData.TransformAccessArray);
// 取得ParentRotation
handle = new UpdateParentRotationJob
{
ParentRotations = this._parentRotations,
}.Schedule(this._springBoneJobData.ParentTransformAccessArray, handle); // 上一个作业结束时的处理
// 物理演算
handle = new LogicJob
{
ImmutableNodeParams = this._springBoneJobData.ImmutableNodeParams,
ParentRotations = this._parentRotations,
DeltaTime = Time.deltaTime,
ColliderHashMap = this._colliderHashMap,
VariableNodeParams = this._springBoneJobData.VariableNodeParams,
}.Schedule(this._springBoneJobData.TransformAccessArray, handle); // 上一个作业结束时的处理
// 将JobHandle保留在字段中以等待下一帧
this._jobHandle = handle;
JobHandle.ScheduleBatchedJobs();
}
执行时,WorkerThreads列表如下。(2)
正在执行的作业的处理在绘制红线的定时附近完成,并切换到下一处理。
CombineDependencies
再回顾一下Job的依赖性,它是“Collider计算/父轮换获取→物理操作”,但实际上前两个 Job 不依赖于彼此,因此它们可以同时执行。
在下一个过程中,“Update Collider”和“Get Parent's Rotation” JobHandle.CombineDependencies合并为一个JobHandle并更改为构建依赖项。
基于这些,可以如下重写它。(目前管理的来源是↓的处理)
CentralizedJobScheduler.cs
// MonoBehaviour.LateUpdate()调用
void ExecuteJobs()
{
// 缓冲区初始化等(处理省略)
.........................
// Collider更新
var updateColliderHashJobHandle = new UpdateColliderHashJob
{
GroupParams = this._colliderGroupJobData.GroupParams,
ColliderHashMap = this._colliderHashMap.ToConcurrent(),
}.Schedule(this._colliderGroupJobData.TransformAccessArray);
// 取得ParentRotation
var updateParentRotationJobHandle = new UpdateParentRotationJob
{
ParentRotations = this._parentRotations,
}.Schedule(this._springBoneJobData.ParentTransformAccessArray);
// 「Collider更新」与「取得ParentRotation」相互之间没有依赖,所以可以合并
var preJobHandle = JobHandle.CombineDependencies(updateColliderHashJobHandle, updateParentRotationJobHandle);
// 物理演算
var handle = new LogicJob
{
ImmutableNodeParams = this._springBoneJobData.ImmutableNodeParams,
ParentRotations = this._parentRotations,
DeltaTime = Time.deltaTime,
ColliderHashMap = this._colliderHashMap,
VariableNodeParams = this._springBoneJobData.VariableNodeParams,
}.Schedule(this._springBoneJobData.TransformAccessArray, preJobHandle); // 「Collider更新」与「取得ParentRotation」同时执行 → 完成后处理物理操作
// 将JobHandle保留在字段中以等待下一帧
_jobHandle = handle;
JobHandle.ScheduleBatchedJobs();
}
通过这样做,WorkerThreads的顺序变为如下[2],并且可以确认无条件地执行「Collider更新」与「取得ParentRotation」而无需等待彼此的结果。
这样,在使用JobSystem实现函数时,组织数据依赖性可能更有效
3、关于Job的执行时间
使用JobSystem时,考虑到执行时序和同步时序,它被认为是能够充分利用免费WorkerThread的策略之一。
※因为在MainThread一侧的绘图(Camera.Render等)周围进行处理时,通常是WorkerThread是免费的。
JobSpringBone的设计控制如下,以便在MainThread端执行绘图处理时使用免费的WorkerThread。
- 1. 在LateUpdate安排每个Job
- 2. JobHandle.ScheduleBatchedJobs执行 Job
-
- ※工作不仅仅按时间表执行。通过调用Complete上述方法来调用或JobHandle.ScheduleBatchedJobs是必要的。
- 3. Complete通过在下一帧调用LateUpdate进行同步。同步后,返回1。
之后,[DefaultExecutionOrder(11000)]可以考虑与诸如FinalIK之类的其他资产的执行顺序控制来设置调度程序本身,或者最新版本3中的行为如下。(如果你看一下WorkerThread,你可以确认围绕物理操作的处理是否适合PostLateUpdate.UpdateAllRenderers时间)
CentralizedJobScheduler.cs
void LateUpdate()
{
// ★ 已执行作业的同步
this._jobHandle.Complete();
// m_center更新
if (this._updateCenterBones.Count > 0)
{
foreach (var springBone in this._updateCenterBones)
{
springBone.UpdateCenterMatrix();
}
}
// Job Schedule & 执行
this.ExecuteJobs();
}
void ExecuteJobs()
{
if (this._springBoneJobData.Length <= 0) return;
if (!this._colliderHashMap.IsCreated)
{
// 对Collider 的初始化
this._colliderHashMap = new NativeMultiHashMap<int, SphereCollider>(
this._colliderHashMapLength, Allocator.Persistent);
}
else
{
this._colliderHashMap.Clear();
}
if (this._colliderHashMap.Capacity != this._colliderHashMapLength)
{
this._colliderHashMap.Dispose();
// 对Collider 的初始化
this._colliderHashMap = new NativeMultiHashMap<int, SphereCollider>(
this._colliderHashMapLength, Allocator.Persistent);
}
// ★ Job Schedule
{
// Collider更新
var updateColliderHashJobHandle = new UpdateColliderHashJob
{
GroupParams = this._colliderGroupJobData.GroupParams,
ColliderHashMap = this._colliderHashMap.ToConcurrent(),
}.Schedule(this._colliderGroupJobData.TransformAccessArray);
// 取得ParentRotation
var updateParentRotationJobHandle = new UpdateParentRotationJob
{
ParentRotations = this._parentRotations,
}.Schedule(this._springBoneJobData.ParentTransformAccessArray);
// 物理演算
this._jobHandle = new LogicJob
{
ImmutableNodeParams = this._springBoneJobData.ImmutableNodeParams,
ParentRotations = this._parentRotations,
DeltaTime = Time.deltaTime,
ColliderHashMap = this._colliderHashMap,
VariableNodeParams = this._springBoneJobData.VariableNodeParams,
}.Schedule(this._springBoneJobData.TransformAccessArray,
JobHandle.CombineDependencies(updateColliderHashJobHandle, updateParentRotationJobHandle));
}
// ★ 工作执行
// ※执行的Job在下一帧的LateUpdate的同步
JobHandle.ScheduleBatchedJobs();
}
4、其他提示
检查性能时,基本上构建然后检查
这不仅限于JobSystem,但在使用NativeContainer时,Profiler上的操作和构建后的操作可能会大大改变Profiler的结果。如果它是“按模型单元将数据传递给Job后调度Job”的类型的实现,那就非常了不起。
- Editor执行时
- 在构建时
如您所见LateUpdate,大量GC.Alloc在编辑器执行中运行,因此,只有Schedule大约需要18到19毫秒。
另一方面,GC.Alloc在构建执行中完全丢失,并且经过的时间大约是6到7毫秒。
其中一个原因是,只有在创建NativeContainer时创建Editor时,才会为内存泄漏跟踪创建一些引用类型的对象,因此,它会影响托管堆。它被认为是
由于这些跟踪对象在构建时都被删除,因此构建执行结果是零分配的。
另外,因为NativeArray等在IL2CPP时受到优化,所以在构建基本上作为标题之后似乎应该确认。
这并不意味着编辑器执行时的Profiler根本不被使用。就个人而言,它用于在执行Job时确认WorkerThread等的顺序。
在任何情况下,建议在了解上述规格后进行测量。
5、利用指针
引用类型不能包含在作业结构或存储在NativeArray中的结构中。
因此,当传递成为共同参数的数据类等时,需要设计一些东西。
如果您只是实现,有一种方法可以传输参数以将每个实例引用到结构并使用NativeArray对其进行管理,但这是传递复制的值,因此在更改作为复制源的公共值时重新绘制NativeArray持有的缓冲区可能会有麻烦。
如果可以毫无问题地操作它可能是好的,但如果频繁更新公共值可能会有点麻烦。
因此,一种可能的解决方案可能是保护NativeMemory中的公共值→通过将指针包含在每个实例的结构中来引用指针。
在此操作中,可以通过更新作业调度等之前指针所指向的存储区域的值来更新整个值而无需更改NativeArray,因此可以执行类似于引用类型的操作是的。
它可能有点令人困惑,因为它只是对单词的评论......但关于这些的操作,因为它在以下文章中进行了总结,请看它是否好。
参考/相关网站
-
- 虽然去年的文章,由于JobSystem的基础没有显着改变,内容本身仍然被认为是有用的。
- 我也有VRMSpringBone的JobSystem支持,或者我记得当我在响应之后重读它时,我有一个难看的理解。。
-
- BurstCompiler 和 Unity.Mathematics
- 因为看一下这个标题很有意思,如果你感到焦虑,可能最好再看一次。