并行化job
ParallelFor jobsdocs.unity3d.com
当调度Jobs时,只能有一个job来进行一项任务。在游戏中,非常常见的情况是在一个庞大数量的对象上执行一个相同的操作。这里有一个独立的job类型叫做IJobParallelFor来处理此类问题。ParallelFor jobs当调度Jobs时,只能有一个job来进行一项任务。在游戏中,非常常见的情况是在一个庞大数量的对象上执行一个相同的操作。这里有一个独立的job类型叫做IJobParallelFor来处理此类问题。
注意:“并行化”job是Unity中所有实现了IJobParallelFor接口的结构的总称。
一个并行化job使用一个NativeArray存放数据来作为它的数据源。并行化job横跨多个核心执行。每个核心上有一个job,每个job处理一部分工作量。IJobParallelFor的行为很类似于IJob,但是不同于只执行一个Execute方法,它会在数据源的每一项上执行Execute方法。Execute方法中有一个整数型的参数。这个索引是为了在job的具体操作实现中访问和操作数据源上的单个元素。
一个定义并行化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;}
}
调度并行化job
当调度并行化job时,你必须指定你分割NativeArray数据源的长度。在结构中同时存在多个NativeArrayUnity时,C# Job System不知道你要使用哪一个NativeArray作为数据源。这个长度同时会告知C# Job System有多少个Execute方法会被执行。
在这个场景中,并行化job的调度会更复杂。当调度并行化任务时,C# Job System会将工作分成多个批次,分发给不同的核心来处理。每一个批次都包含一部分的Execute方法。随后C# Job System会在每个CPU核心的Unity原生Job System上调度最多一个job,并传递给这个job一些批次的工作来完成。
一个并行化job划分批次到多个CPU核心
当一个原生job提前完成了分配给它的工作批次后,它会从其他原生job那里获取其剩余的工作批次。它每次只获取那个原生job剩余批次的一半,为了确保缓存局部性(cache locality)。
为了优化这个过程,你需要指定一个每批次数量(batch count)。这个每批次数量控制了你会生成多少job和线程中进行任务分发的粒度。使用一个较低的每批次数量,比如1,会使你在线程之间的工作分配更平均。它会带来一些额外的开销,所以有时增加每批次数量会是更好的选择。从每批次数量为1开始,然后慢慢增加这个数量直到性能不再提升是一个合理的策略。
调度并行化job的例子:
job代码
// Job adding two floating point values together
public struct MyParallelJob : IJobParallelFor
{[ReadOnly]public NativeArray<float> a;[ReadOnly]public NativeArray<float> b;public NativeArray<float> result;public void Execute(int i){result[i] = a[i] + b[i];}
}
主线程代码:
NativeArray<float> a = new NativeArray<float>(2, Allocator.TempJob);NativeArray<float> b = new NativeArray<float>(2, Allocator.TempJob);NativeArray<float> result = new NativeArray<float>(2, Allocator.TempJob);a[0] = 1.1;
b[0] = 2.2;
a[1] = 3.3;
b[1] = 4.4;MyParallelJob jobData = new MyParallelJob();
jobData.a = a;
jobData.b = b;
jobData.result = result;// Schedule the job with one Execute per index in the results array and only 1 item per processing batch
JobHandle handle = jobData.Schedule(result.Length, 1);// Wait for the job to complete
handle.Complete();// Free the memory allocated by the arrays
a.Dispose();
b.Dispose();
result.Dispose();
ParallelForTransform jobs
https://docs.unity3d.com/Manual/JobSystemParallelForTransformJobs.html
一个ParallelForTransform job是另一个类型的ParallelFor job;它是专门为了Transforms上的操作设计的。
注意:ParallelForTransform job是Unity中所有实现了IJobParallelForTransform接口的结构的总称。
C# Job System建议和故障排除
C# Job System tips and troubleshootingdocs.unity3d.com
当你使用Unity C# Job System时,确保你遵守以下几点:C# Job System tips and troubleshooting当你使用Unity C# Job System时,确保你遵守以下几点:
不要从一个job中访问静态的数据
在所有的安全性系统中你都应当避免从一个job中访问静态数据。如果你访问了错误的数据,你可能会使Unity崩溃,通常是以意想不到的方式。举例来说,访问一个MonoBehaviour可以导致域重新加载时崩溃。
注意:因为这个风险,未来版本的Unity会通过静态分析来阻止全局变量在job中的访问。如果你确实在job中访问了静态数据,你应当预见到你的代码会在Unity未来的版本中报错。
刷新已调度的批次
当你希望你的job开始执行时,你可以通过JobHandle.ScheduleBatchedJobs来刷新已调度的批次。注意调用这个接口时会对性能产生负面的影响。不刷新批次将会延迟调度job,直到主线程开始等待job的结果。在任何其他情况中,你应当调用JobHandle.Complete来开始执行过程。
注意:在Entity Component System(ECS)中批次会暗中为你进行刷新,所以调用JobHandle.ScheduleBatchedJobs是不必要的。
不要试图去更新NativeContainer的内容
由于缺乏引用返回值,不可能去直接修改一个NativeContainer的内容。例如,nativeArray[0]++ ;和 var temp = nativeArray[0]; temp++;一样,都没有更新nativeArray中的值。
你必须从一个index将数据拷贝到一个局部临时副本,修改这个副本,并将它保存回去,像这样:
MyStruct temp = myNativeArray[i];
temp.memberVariable = 0;
myNativeArray[i] = temp;
调用JobHandle.Complete来重新获得归属权
在主线程重新使用数据前,追踪数据的所有权需要依赖项都完成。只检查JobHandle.IsCompleted是不够的。你必须调用JobHandle.Complete来在主线程中重新获取NaitveContainer类型的所有权。调用Complete同时会清理安全性系统中的状态。不这样做的话会造成内存泄漏。这个过程也在你每一帧都调度依赖于上一帧job的新job时被采用。
在主线程中调用Schedule和Complete
你只能在主线程中调用Schedule和Complete方法。如果一个job需要依赖于另一个,使用JobHandle来处理依赖关系而不是尝试在job中调度新的job。
在正确的时间调用Schedule和Complete
一旦你拥有了一个job所需的数据,尽可能快地在job上调用Schedule,在你需要它的执行结果之前不要调用Complete。一个良好的实践是调度一个你不需要等待的job,同时它不会与当前正在运行的其他job产生竞争。举例来说,如果你在一帧结束和下一帧开始之前拥有一段没有其他job在运行的时间,并且可以接受一帧的延迟,你可以在一帧结束的时候调度一个job,在下一帧中使用它的结果。或者,如果这个转换时间已经被其他job占满了,但是在一帧中有一大段未充分利用的时段,在这里调度你的job会更有效率。
将NativeContainer标记为只读的
记住job在默认情况下拥有NativeContainer的读写权限。在合适的NativeContainer上使用[ReadOnly]属性可以提升性能。
检查数据的依赖
在Unity的Profiler窗口中,主线程中的"WaitForJobGroup"标签表明了Unity在等待一个工人线程上的job结束。这个标签可能意味着你以某种方式引入了一个资源依赖,你需要去解决它。查找JobHandle.Complete来追踪你在什么地方有资源依赖,导致主线程必须等待。
调试job
job拥有一个Run方法,你可以用它来替代Schedule从而让主线程立刻执行这个job。你可以使用它来达到调试目的。
不要在job中开辟托管内存
在job中开辟托管内存会难以置信得慢,并且这个job不能利用Unity的Burst编译器来提升性能。Burst是一个新的基于LLVM的后端编译器技术,它会使事情对于你更加简单。它获取C# job并利用你平台的特定功能产生高度优化的机器码。
更多信息
- 观看Unity GDC 2018: C# Job System的片段列表
- 获取C# Job Syetem与ECS交互的更进一步信息,查看ECS package documentation on GitHub