Jobs And Scheduling¶
Job Types¶
flowchart LR
subgraph ImplicitDependency
Job.WithCode
IJobEntity
IJobParallelFor
IJobParallelForTransform
end
subgraph ExplicitDependency
IJobChunk
end
subgraph Deprecating
Entities.ForEach
end
style Entities.ForEach fill:red
Code Generation¶
flowchart LR
IJobEntity --> IJobChunk
Entities.ForEach --> IJobChunk
style Entities.ForEach fill:red
Jobs And Systems¶
flowchart LR
subgraph System1
Job1
Job2
end
subgraph System2
Job3
end
Implicit vs Explicit Dependency¶
Implicit¶
flowchart LR
subgraph NewlyScheduledJob
ABC{{ABC}} --> Job --> XYZ{{XYZ}}
end
subgraph PrevScheduledJobs
subgraph Group1
WriteToA
WriteToB
WriteToC
end
subgraph Group2
ReadOrWriteToX
ReadOrWriteToY
ReadOrWriteToZ
end
AnotherJob
end
Group1 --> NewlyScheduledJob
Group2 --> NewlyScheduledJob
flowchart LR
subgraph System1
JobAB
JobAC
end
subgraph System2
JobBC
end
CompA{{CompA}} --> JobAB
JobAB --> CompB{{CompB}}
CompA --> JobAC --> CompC{{CompC}}
CompB --> JobBC --> CompC
- This is what implicit dependency wants to achieve, IN THEORY
- IN REALITY, dependencies are automatically managed per system, not per job
- Within a system, all implicitly scheduled jobs form a sequential chain (link)
// implicit
new Job{}.Schedule();
// would translate to
Dependency = new Job{}.Schedule(Dependency);
// implicit
new JobAB{}.Schedule();
new JobAC{}.Schedule();
// would translate to
Dependency = new JobAB{}.Schedule(Dependency);
Dependency = new JobAC{}.Schedule(Dependency);
// which means
var handleAB = new JobAB{}.Schedule(Dependency);
Dependency = new JobAC{}.Schedule(handleAB);
flowchart LR
subgraph System1
JobAB --> JobAC
end
subgraph System2
JobBC
end
PrevFrame{{Prev Frame}} --> System1
System1 --> ABC{{AB->C}} --> System2
System2 --> NextFrame{{Next Frame}}
- To fully utilize implicit dependency, one system should only schedule one job
flowchart LR
subgraph System1
JobAB
end
subgraph System2
JobAC
end
subgraph System3
JobBC
end
PrevFrame{{Prev Frame}} --> System1
PrevFrame --> System2
System1 --> AB{{A->B}}
System2 --> AC{{A->C}}
AB --> System3
AC --> System3
System3 --> NextFrame{{Next Frame}}
Explicit¶
Are required for:
flowchart LR
IJobChunk
flowchart LR
Job1 -- write --> ContainerA{{Container A}} -- read --> Job2
Job1 -- need explicit --> Job2
Code example:
var array = new NativeArray(Allocator.TempJob);
var handle1 = new Job1{
Output = array,
}.Schedule(Dependency);
Dependency = new Job2{
Input = array,
}.Schedule(handle1);
array.Dispose(Dependency);
Schedule vs ScheduleParallel¶
flowchart TB
subgraph AlwaysScheduleParallel
IJobParallelFor
IJobParallelForTransform
end
subgraph HasScheduleParallel
IJobEntity
IJobChunk
end
subgraph NoScheduleParallel
Job.WithCode
end
- ScheduleParallel will split execution of the job to multiple Worker Threads
- The Job completes when all threads of its execution complete
IJobParallelFor¶
struct AddJob: IJobParallelFor
{
public NativeArray<float> values;
public float increase;
public void Execute (int index)
{
values[index] += increase;
}
}
var array = new NativeArray<float>(Allocator.TempJob);
...
Dependency = new AddJob{
values = array,
increase = 1,
}.Schedule(array.Length, innerLoopBatchCount: 4, Dependency);
array.Dispose(Dependency);
innerLoopBatchCount
should be in power of 2, to take advantage of vectorization when BurstCompile
is used
- Expensive job could use smaller innerLoopBatchCount
flowchart LR
Schedule["Schedule(length:8, batch:2)"]
Schedule --> Batch01
Schedule --> Batch23
Schedule --> Batch45
Schedule --> Batch67
subgraph NativeJob0
Batch01
Batch23
end
subgraph NativeJob1
Batch45
Batch67
end
NativeJob0 --> Core0
NativeJob1 --> Core1
IJobEntity¶
partial struct DoubleJob: IJobEnttiy {
private void Execute(in CompA compA, ref CompB compB) {
compB.Value = 2 * compA.Value;
}
}
new DoubleJob{}.ScheduleParallel();
implicit query
derived from the Execute
method
- Behind the scene it is compiled into IJobChunk
flowchart LR
DoubleJob --> Query
Query --> Chunk0 --> Core0
Query --> Chunk1 --> Core1
- Use EntityIndexInQuery
to fill values for newly created entities. This is similar to IJobParallelFor
, but is more memory efficient because entities are processed in chunks.
partial struct FillJob: IJobEntity {
public NativeArray<float> Inputs;
private void Execute(
[EntityIndexInQuery] int index,
ref CompA compA){
compA.Value = Inputs[index];
}
}
var values = new NativeArray<float>(100, Allocator.TempJob);
new SomeJob{
Outputs = values,
}.ScheduleParallel();
EntityManager.Instantiate(prefab, inputs.Length, Allocator.Temp);
new FillJob{
Inputs = values,
}.ScheduleParallel();
IJobChunk¶
- IJobEntity doesn't support Generic, making reusing code not possible
- Use IJobChunk in this case
interface IComp : IComponentData {
float GetComputedValue();
}
abstract partial class AbstractSystem<TComp> : SystemBase
where TComp: unmanaged, IComp
{
private struct Job : IJobChunk
{
[ReadOnly] public ComponentTypeHandle<TComp> Inputs;
public ComponentTypeHandle<FloatComp> Outputs;
public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask,
in v128 chunkEnabledMask)
{
var inputs = chunk.GetNativeArray(ref Inputs);
var outputs = chunk.GetNativeArray(ref Outputs);
var enumerator = new ChunkEntityEnumerator(useEnabledMask, chunkEnabledMask, chunk.Count);
while (enumerator.NextEntityIndex(out var i))
{
var input = inputs[i];
outputs[i] = input.GetComputedValue();
}
}
}
private EntityQuery _query;
protected override void OnCreate()
{
_query = EntityManager.CreateEntityQuery(new EntityQueryDesc
{
All = new ComponentType[] { typeof(TComp), typeof(FloatComp) },
});
}
protected override void OnUpdate()
{
Dependency = new Job
{
Inputs = SystemAPI.GetComponentTypeHandle<TComp>(true),
Outputs = SystemAPI.GetComponentTypeHandle<FloatComp>(),
}.ScheduleParallel(_query, Dependency);
}
}
struct Add: IComp {
public float A;
public float B;
public float GetComputedValue() => A + B;
}
struct Mul: IComp {
public float A;
public float B;
public float GetComputedValue() => A * B;
}
internal partial class AddSystem: AbstractSystem<Add>{}
internal partial class MulSystem: AbstractSystem<Mul>{}
CompleteDependency¶
Avoid doing this to increase parallelismStructural Changes¶
// will automatically call Dependency.Complete before execute
EntityManager.CreateEntity
EntityManager.Instantiate
EntityManager.DestroyEntity
EntityManager.AddComponent
EntityManager.RemoveComponent
EntityManager.SetSharedComponent
ecb.Playback(EntityManager) //if the ecb is not empty and constains one of the above