Job System 与 Burst¶
第一次用 Job¶
刚开始用 Job System 时,我把一个循环改成 Job,然后在同一行 Complete(),结果不仅没变快,主线程还多了一次调度开销。
后来才明白,Job 的收益来自“把可并行的计算挪走,并且在真正需要结果之前不等待”。如果提交完马上等结果,等于只是换了一个写法。1
一个最小 Job¶
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
[BurstCompile]
public struct IntegrateJob : IJobParallelFor
{
[ReadOnly] public NativeArray<float3> Velocity;
public NativeArray<float3> Position;
public float DeltaTime;
public void Execute(int index)
{
Position[index] += Velocity[index] * DeltaTime;
}
}
调度时不要立刻 Complete:
var job = new IntegrateJob
{
Velocity = _velocity,
Position = _position,
DeltaTime = Time.deltaTime
};
_integrateHandle = job.Schedule(_position.Length, 64);
64 是每批处理的数量。数量太小,调度次数会变多;数量太大,负载不容易平均到各个 Worker。可以先用 32、64、128 测一轮,看实际耗时再定。
等主线程真的需要读 _position 时再完成:
如果后面的 Job 依赖这一步,可以把 Handle 传进去,让它自己排顺序:
var sortHandle = sortJob.Schedule(_integrateHandle);
var uploadHandle = uploadJob.Schedule(sortHandle);
不要在中间随便插入 Complete,否则后面的任务又会被拉回主线程。
Burst 能做什么¶
Burst 适合处理纯数据计算,例如位置积分、视锥裁剪、寻路评分和批量插值。它不能直接操作 GameObject、Transform、MonoBehaviour,也不适合依赖反射或托管异常。
使用 [BurstCompile] 后,最好打开 Burst Inspector 看生成的代码和编译日志。仅仅加上特性不代表一定编译成功,也不代表一定比手写代码快。2
| 适合 | 不适合 |
|---|---|
NativeArray<T>、float3、数学函数 | 托管对象和大部分 Unity API |
| 固定大小的纯数据计算 | 动态分配、反射、异常处理 |
| 批量循环和 SIMD | 逻辑复杂但数据量很小的代码 |
数据布局比线程数更重要¶
如果 Job 只需要位置和速度,就不要把整个角色对象都塞进去。把同类字段放在连续内存里,缓存命中率会好很多。
// AoS:一个结构体里放所有字段
public struct Agent
{
public float3 Position;
public float3 Velocity;
public int Health;
public int Team;
}
// SoA:按字段拆成多个数组
public NativeArray<float3> Positions;
public NativeArray<float3> Velocities;
SoA 不是所有情况都更好。数据量小、访问模式不固定时,AoS 更直观,代码也更好维护。只有在热点循环里反复批量读取同一组字段时,拆分才有明显收益。
Native 容器的生命周期¶
| Allocator | 适合 |
|---|---|
Allocator.Temp | 单次调用,不能跨帧 |
Allocator.TempJob | 短时间 Job 链,需要尽快释放 |
Allocator.Persistent | 长期 Buffer,需要手动 Dispose |
NativeArray<T> 不受 GC 管理,但忘记释放一样会泄漏。3 长期 Buffer 最好交给一个明确的系统管理,在 OnDestroy 或场景卸载时统一释放。
什么时候不用 Job¶
- 数据只有几十个,调度成本比计算还高。
- 逻辑必须访问场景对象,搬到 Job 里只会增加同步。
- 每帧都要马上读结果,无法把等待藏到其他工作后面。
- 数据结构还没稳定,过早并行会让调试变难。
先让 Profiler 证明主线程确实是瓶颈,再决定要不要把代码改成 Job。否则很容易把一段好调试的代码,换成一段更难维护的并行代码。