Unity JobSystem 原理初探(1) - Job.Execute()
- 本文分析了JobSystem中IJobParallelFor的多线程实现
- 代码来源:C#部分 C++部分来自于IDA pro解析UnityPlayer.dll
Unity中的C#脚本如下:
public class MyJobBehavior : MonoBehaviour
{
void Update()
{
var input = new NativeArray<float>(10000, Allocator.Persistent);
var output = new NativeArray<float>(10000, Allocator.Persistent);
for (int i = 0; i < input.Length; i++)
input[i] = 1.0f * i;
var job = new MyJob
{
Input = input,
Output = output
};
job.Schedule(10000,64).Complete();
input.Dispose();
output.Dispose();
}
private struct MyJob : IJobParallelFor
{
[ReadOnly]
public NativeArray<float> Input;
[WriteOnly]
public NativeArray<float> Output;
public void Execute(int i)
{
Output[i] = Input[i]*100;
}
}
}
我先来研究job.Schedule,再C#接口调用如下,可以看到JobsUtility提供了进入Unity内核入口.
unsafe public static JobHandle Schedule<T>(this T jobData, int arrayLength, int innerloopBatchCount, JobHandle dependsOn = new JobHandle()) where T : struct, IJobParallelFor
{
var scheduleParams = new JobsUtility.JobScheduleParameters(UnsafeUtility.AddressOf(ref jobData), ParallelForJobStruct<T>.jobReflectionData, dependsOn, ScheduleMode.Parallel);
return JobsUtility.ScheduleParallelFor(ref scheduleParams, arrayLength, innerloopBatchCount);
}
public static extern JobHandle ScheduleParallelFor(ref JobScheduleParameters parameters, int arrayLength, int innerloopBatchCount);
C++中的逆向代码如下,
JobsUtility_ScheduleParallelFor_Injected_m0F7C3AB0415EF402D7242BE6FA82F2ACBC899360
Unity.Jobs.LowLevel.Unsafe.JobsUtility::ScheduleParallelFor_Injected(Unity.Jobs.LowLevel.Unsafe.JobsUtility/JobScheduleParameters&,System.Int32,System.Int32,Unity.Jobs.JobHandle&)
void __fastcall JobsUtility_CUSTOM_ScheduleParallelFor_Injected(
struct JobScheduleParameters *a1,
unsigned int a2,
unsigned int a3,
struct JobFence *a4)
{
...
v16 = (const void *)ScheduleManagedJobParallelFor(v10, a1, a2, a3, v8);
...
}
ScheduleManagedJobParallelFor是非常重要的函数
_QWORD *__fastcall ScheduleManagedJobParallelFor(
_QWORD *a1,
__int64 a2,
unsigned int a3,
int a4,
struct ScriptingExceptionPtr *a5)
{
__int64 *v5; // rdi
__int64 i; // rcx
__int64 v8; // [rsp+0h] [rbp-C08h] BYREF
char v9[2616]; // [rsp+50h] [rbp-BB8h] BYREF
void *v10; // [rsp+A88h] [rbp-180h] BYREF
char v11[64]; // [rsp+AA8h] [rbp-160h] BYREF
char v12[48]; // [rsp+AE8h] [rbp-120h] BYREF
_QWORD v13[3]; // [rsp+B18h] [rbp-F0h] BYREF
char v14[12]; // [rsp+B34h] [rbp-D4h] BYREF
char v15[16]; // [rsp+B40h] [rbp-C8h] BYREF
__int64 v16; // [rsp+B50h] [rbp-B8h] BYREF
int v17; // [rsp+B58h] [rbp-B0h]
__int64 v18; // [rsp+B60h] [rbp-A8h] BYREF
int v19; // [rsp+B68h] [rbp-A0h]
char v20[16]; // [rsp+B70h] [rbp-98h] BYREF
struct ScriptingExceptionPtr *v21; // [rsp+B80h] [rbp-88h]
_QWORD *v22; // [rsp+B90h] [rbp-78h]
_QWORD *v23; // [rsp+B98h] [rbp-70h]
void *v24; // [rsp+BA0h] [rbp-68h]
int v25; // [rsp+BA8h] [rbp-60h]
JobBatchDispatcher *v26; // [rsp+BB0h] [rbp-58h]
__int64 *v27; // [rsp+BB8h] [rbp-50h]
__int64 v28; // [rsp+BC0h] [rbp-48h]
__int64 *v29; // [rsp+BC8h] [rbp-40h]
__int64 *v30; // [rsp+BD0h] [rbp-38h]
_QWORD *v31; // [rsp+BD8h] [rbp-30h]
BOOL v32; // [rsp+BE0h] [rbp-28h]
BOOL v34; // [rsp+BE8h] [rbp-20h]
v5 = &v8;
for ( i = 766i64; i; --i )
{
*(_DWORD *)v5 = -858993460;
v5 = (__int64 *)((char *)v5 + 4);
}
if ( a3 || (unsigned __int8)RequiresCleanupBuffers(*(_QWORD *)(a2 + 24)) )
{
Validate_Before_Schedule((const struct JobScheduleParameters *)a2, a3, 0i64, a5);
v21 = a5;
v32 = *(_QWORD *)a5 != 0i64;
if ( v32 || *((_QWORD *)a5 + 1) )
{
v22 = a1;
*a1 = 0i64;
*((_DWORD *)v22 + 2) = 0;
return a1;
}
else
{
BatchAllocator::BatchAllocator((BatchAllocator *)v9);
AllocateManagedJobData(v9, &v10, a2, 0i64);
AllocateWorkStealingRange((struct BatchAllocator *)v9, a3, a4, (struct WorkStealingAllocationData *)v11, -1);
qmemcpy(v14, (const void *)GetJobAllocator(v15, a2), sizeof(v14));
qmemcpy(v12, v14, 0xCui64);
BatchAllocator::Commit((BatchAllocator *)v9, (const struct MemLabelId *)v12);
qmemcpy(v20, v12, 0xCui64);
InitializeManagedJobData((_DWORD)v10, a2, 0, 0, (__int64)v20);
InitializeWorkStealingRange(
(const struct WorkStealingAllocationData *)v11,
(struct WorkStealingRange *)((char *)v10 + 48));
if ( *(_DWORD *)(a2 + 16) == 1 )
{
v23 = v13;
v13[0] = 0i64;
LODWORD(v13[1]) = 0;
v25 = *((_DWORD *)v10 + 13);
v24 = v10;
v26 = gBatchScheduler;
JobBatchDispatcher::ScheduleJobForEachInternal(
gBatchScheduler,
(struct JobFence *)v13,
ForwardJobForEachToManaged,
v10,
v25,
ForwardJobForEachCleanup,
(const struct JobFence *)a2);
Validate_After_Schedule((const struct JobFence *)v13, (const struct JobScheduleParameters *)a2);
qmemcpy(a1, v13, 0x10ui64);
return a1;
}
else
{
if ( *((_DWORD *)v10 + 13) != 1
&& AssertImplementation(
0,
"C:\\buildslave\\unity\\build\\Runtime/Jobs/ScriptBindings/JobsBindings.cpp",
895,
-1,
"Assertion failed on expression: 'jobData->range.numJobs == 1'")
&& (unsigned __int8)Baselib_Debug_IsDebuggerAttached() )
{
__debugbreak();
}
v27 = &v16;
v16 = 0i64;
v17 = 0;
v29 = &v16;
v28 = a2;
v34 = !*(_QWORD *)a2 && *(_DWORD *)(v28 + 8) == *((_DWORD *)v29 + 2);
if ( !v34
&& AssertImplementation(
0,
"C:\\buildslave\\unity\\build\\Runtime/Jobs/ScriptBindings/JobsBindings.cpp",
896,
-1,
"Assertion failed on expression: 'jobParameters.dependency == JobFence()'") )
{
if ( (unsigned __int8)Baselib_Debug_IsDebuggerAttached() )
__debugbreak();
}
v30 = &v18;
v18 = 0i64;
v19 = 0;
Validate_After_Schedule((const struct JobFence *)&v18, (const struct JobScheduleParameters *)a2);
ForwardJobForEachToManaged(v10, 0);
ForwardJobForEachCleanup(v10);
v31 = a1;
*a1 = 0i64;
*((_DWORD *)v31 + 2) = 0;
return a1;
}
}
}
else
{
qmemcpy(a1, (const void *)a2, 0x10ui64);
return a1;
}
}
JobBatchDispatcher::ScheduleJobForEachInternal这个函数很关键,再SRP原理初探我也提到过.是Unity大多数多线程操作的调用入口.
看重要的回调函数ForwardJobForEachToManaged
void __fastcall ForwardJobForEachToManaged(void *a1, int a2)
{
__int64 *v2; // rdi
__int64 i; // rcx
__int64 v4; // [rsp+0h] [rbp-68h] BYREF
void (__stdcall *NativeJobFunction)(void *, void *, void *, void *, int); // [rsp+20h] [rbp-48h]
struct profiling::Marker *v6; // [rsp+38h] [rbp-30h]
const struct JobReflectionData *v7; // [rsp+48h] [rbp-20h]
struct profiling::Marker *v8; // [rsp+50h] [rbp-18h]
__int64 v9; // [rsp+58h] [rbp-10h]
v2 = &v4;
for ( i = 24i64; i; --i )
{
*(_DWORD *)v2 = -858993460;
v2 = (__int64 *)((char *)v2 + 4);
}
v9 = -2i64;
NativeJobFunction = GetNativeJobFunction(*(const struct JobReflectionData **)a1, 0);
v7 = *(const struct JobReflectionData **)a1;
if ( NativeJobFunction )
v8 = (struct profiling::Marker *)*((_QWORD *)v7 + 42);
else
v8 = (struct profiling::Marker *)*((_QWORD *)v7 + 43);
v6 = v8;
profiler_begin(v8);
ExecuteJobCopyData(
(const void **)a1,
(void (__fastcall *)(_QWORD, _QWORD, _QWORD, _QWORD, _DWORD))NativeJobFunction,
a2);
profiler_end(v6);
}
继续向下深入可以看到
ExecuteJobCopyData
ExecuteJob_0
ScriptingInvocation::Invoke
scripting_method_invoke
il2cpp_runtime_invoke
gameassembly.il2cpp::vm::Runtime::Invoke
gameassembly.ExecuteJobFunction_Invoke
gameassembly._MyJob_Execute
最终会调用gameassembly._MyJob_Execute就是原来脚本中
总结:
整个Unity JobSystem调用过程
job.Schedule//gameassembly.dll
JobsUtility.ScheduleParallelFor
JobsUtility_CUSTOM_ScheduleParallelFor_Injected//来到UnityPlayer.dll
ScheduleManagedJobParallelFor
JobBatchDispatcher::ScheduleJobForEachInternal(//设置线程回调函数
gBatchScheduler,
(struct JobFence *)v13,
ForwardJobForEachToManaged,
v10,
v25,
ForwardJobForEachCleanup,
(const struct JobFence *)a2);
ExecuteJobCopyData
ExecuteJob_0
ScriptingInvocation::Invoke
scripting_method_invoke
il2cpp_runtime_invoke//回到gameassembly.dll
gameassembly.il2cpp::vm::Runtime::Invoke
gameassembly.ExecuteJobFunction_Invoke
gameassembly._MyJob_Execute//最后才是执行回调函数
这就是整个job.execute函数的执行大致过程,很多细节还要进一步探索.