JacksonDunstan.com | Как IL2CPP вызывает Burst (2024)

(Russian translation from English by Maxim Voloshin)

Когда-нибудь задумывались как код, скомпилированный IL2CPP, может вызывать код, скомпилированный Burst? Сегодня мы углубимся в детали и выясним это!

Тест

Давайте напишем маленький скрипт, вывод компиляции которого мы сможем исследовать. Это простенькая задача, скомпилированная Burst компилятором, и запускающая ее функция на C#:

using UnityEngine;using Unity.Jobs;using Unity.Collections;using Unity.Burst;[BurstCompile]struct Job : IJob{ public NativeArray<float> A; public NativeArray<float> B; public NativeArray<float> Sum; public void Execute() { for (int i = 0; i < A.Length; ++i) { Sum[i] = A[i] + B[i]; } }}class TestScript : MonoBehaviour{ void RunJob( NativeArray<float> a, NativeArray<float> b, NativeArray<float> sum) { new Job { A=a, B=b, Sum=sum }.Run(); }}

Теперь соберем билд под macOS и откроем папку PROJECTNAME_macOS_BackUpThisFolder_ButDontShipItWithYourGame/il2cppOutput, чтобы увидеть вывод IL2CPP.

Assembly-CSharp.cpp

Здесь мы нашли функцию RunJob. Для лучшего понимания, что именно происходит в коде, я добавил комментариев и пробелов. Некоторые идентификаторы в именах на самом деле длинные и может потребоваться горизонтальная прокрутка.

IL2CPP_EXTERN_C IL2CPP_METHOD_ATTR void TestScript_RunJob_m8CC0C7D38A7D2356765B2FF3DED6604D147B631C ( TestScript_t292BEAEA5C665F1E649B7CCA16D364E5E836A4D1 * __this, NativeArray_1_t2F93EF84A543D826D53EFEAFE52F5C42392D0D67 ___a0, NativeArray_1_t2F93EF84A543D826D53EFEAFE52F5C42392D0D67 ___b1, NativeArray_1_t2F93EF84A543D826D53EFEAFE52F5C42392D0D67 ___sum2, const RuntimeMethod* method){ // Затратный вызов, в основном в первый раз static bool s_Il2CppMethodInitialized; if (!s_Il2CppMethodInitialized) { il2cpp_codegen_initialize_method (TestScript_RunJob_m8CC0C7D38A7D2356765B2FF3DED6604D147B631C_MetadataUsageId); s_Il2CppMethodInitialized = true; } // Инициализация структуры задачи Job_t38E17FBF016E094161289D2A6FBF5D7E42F4F458 V_0; memset((&V_0), 0, sizeof(V_0)); { // Все поля структуры зануляются il2cpp_codegen_initobj((&V_0), sizeof(Job_t38E17FBF016E094161289D2A6FBF5D7E42F4F458 )); // Присваиваются значения A, B, и Sum полям структуры задачи NativeArray_1_t2F93EF84A543D826D53EFEAFE52F5C42392D0D67 L_0 = ___a0; (&V_0)->set_A_0(L_0); NativeArray_1_t2F93EF84A543D826D53EFEAFE52F5C42392D0D67 L_1 = ___b1; (&V_0)->set_B_1(L_1); NativeArray_1_t2F93EF84A543D826D53EFEAFE52F5C42392D0D67 L_2 = ___sum2; (&V_0)->set_Sum_2(L_2); // Вызов Job.Run(), который, на самом деле, является методом расширением IJobExtensions.Run(Job) // Передача структуры и глобального RuntimeMethod указателя в метод расширение Job_t38E17FBF016E094161289D2A6FBF5D7E42F4F458 L_3 = V_0; IJobExtensions_Run_TisJob_t38E17FBF016E094161289D2A6FBF5D7E42F4F458_m6485C7A28C7D5FF7E849D5BB5E442771CCBDB823( L_3, /*скрытый аргумент*/IJobExtensions_Run_TisJob_t38E17FBF016E094161289D2A6FBF5D7E42F4F458_m6485C7A28C7D5FF7E849D5BB5E442771CCBDB823_RuntimeMethod_var); return; }}

Теперь перейдем к IJobExtensions.Run(Job), который реализует Job.Run():

inline void IJobExtensions_Run_TisJob_t38E17FBF016E094161289D2A6FBF5D7E42F4F458_m6485C7A28C7D5FF7E849D5BB5E442771CCBDB823 ( Job_t38E17FBF016E094161289D2A6FBF5D7E42F4F458 p0, const RuntimeMethod* method){ // Это просто передача аргументов через // глобальный указатель IJobExtensions_Run_TisJob_..._gshared  (( void (*) (Job_t38E17FBF016E094161289D2A6FBF5D7E42F4F458 , const RuntimeMethod*))IJobExtensions_Run_TisJob_t38E17FBF016E094161289D2A6FBF5D7E42F4F458_m6485C7A28C7D5FF7E849D5BB5E442771CCBDB823_gshared)( p0, method);}
GenericMethods1.cpp

Теперь остановимся на упомянутом выше указателе, который ведет к следующей функции:

IL2CPP_EXTERN_C IL2CPP_METHOD_ATTR voidIJobExtensions_Run_TisJob_t38E17FBF016E094161289D2A6FBF5D7E42F4F458_m6485C7A28C7D5FF7E849D5BB5E442771CCBDB823_gshared ( Job_t38E17FBF016E094161289D2A6FBF5D7E42F4F458 ___jobData0, const RuntimeMethod* method){ // Создание и зануление JobScheduleParameters JobScheduleParameters_t55DC8564D72859191CE4E81639EFD25F6C6A698F V_0; memset((&V_0), 0, sizeof(V_0)); // Создание и зануление JobHandle JobHandle_tDA498A2E49AEDE014468F416A8A98A6B258D73D1 V_1; memset((&V_1), 0, sizeof(V_1)); { // Получение указателя #0 для RuntimeMethod, представляющего собой // метод расширение, и вызов его с передачей в качестве аргументов  // структуры задачи и указателя на MethodInfo void* L_0 = (( void* (*) (Job_t38E17FBF016E094161289D2A6FBF5D7E42F4F458 *, const RuntimeMethod*))IL2CPP_RGCTX_METHOD_INFO(method->rgctx_data, 0)->methodPointer)( (Job_t38E17FBF016E094161289D2A6FBF5D7E42F4F458 *)(Job_t38E17FBF016E094161289D2A6FBF5D7E42F4F458 *)(&___jobData0), /*скрытый аргумент*/IL2CPP_RGCTX_METHOD_INFO(method->rgctx_data, 0)); // Получение указателя #1 для RuntimeMethod, представляющего собой // метод расширение, и вызов его с указателя на MethodInfo intptr_t L_1 = (( intptr_t (*) (const RuntimeMethod*))IL2CPP_RGCTX_METHOD_INFO(method->rgctx_data, 1)->methodPointer)( /*скрытый аргумент*/IL2CPP_RGCTX_METHOD_INFO(method->rgctx_data, 1)); // Инициализация JobHandle, созданного выше il2cpp_codegen_initobj((&V_1), sizeof(JobHandle_tDA498A2E49AEDE014468F416A8A98A6B258D73D1 )); // Вызов конструктора JobScheduleParameters, созданного выше // Передает JobHandle и упомянутые указатели JobHandle_tDA498A2E49AEDE014468F416A8A98A6B258D73D1 L_2 = V_1; JobScheduleParameters__ctor_m09A522B620ED79BDFD86DE2544175159B6179E48( (JobScheduleParameters_t55DC8564D72859191CE4E81639EFD25F6C6A698F *)(JobScheduleParameters_t55DC8564D72859191CE4E81639EFD25F6C6A698F *)(&V_0), (void*)(void*)L_0, (intptr_t)L_1, (JobHandle_tDA498A2E49AEDE014468F416A8A98A6B258D73D1 )L_2, (int32_t)0, /*скрытый аргумент*/NULL); // Вызов JobsUtility.Schedule с JobScheduleParameters JobsUtility_Schedule_m544BE1DBAEFF069809AE5304FD6BBFEE2927D4C3( (JobScheduleParameters_t55DC8564D72859191CE4E81639EFD25F6C6A698F *)(JobScheduleParameters_t55DC8564D72859191CE4E81639EFD25F6C6A698F *)(&V_0), /*скрытый аргумент*/NULL); return; }}
UnityEngine.CoreModule.cpp

Далее рассмотрим последний вызов JobsUtility.Schedule, чтобы увидеть как запускается задача в JobSystem.

IL2CPP_EXTERN_C IL2CPP_METHOD_ATTR JobHandle_tDA498A2E49AEDE014468F416A8A98A6B258D73D1JobsUtility_Schedule_m544BE1DBAEFF069809AE5304FD6BBFEE2927D4C3 ( JobScheduleParameters_t55DC8564D72859191CE4E81639EFD25F6C6A698F * ___parameters0, const RuntimeMethod* method){ // Инициализация JobHandle нулями JobHandle_tDA498A2E49AEDE014468F416A8A98A6B258D73D1 V_0; memset((&V_0), 0, sizeof(V_0)); { // Вызов JobsUtility.Schedule_Injected с аргументами JobScheduleParameters // и указателем на JobHandle JobScheduleParameters_t55DC8564D72859191CE4E81639EFD25F6C6A698F * L_0 = ___parameters0; JobsUtility_Schedule_Injected_mC918219A2045C68697BD2C7FCE7DCA515CE09C01( (JobScheduleParameters_t55DC8564D72859191CE4E81639EFD25F6C6A698F *)L_0, (JobHandle_tDA498A2E49AEDE014468F416A8A98A6B258D73D1 *)(&V_0), /*скрытый аргумент*/NULL); // Возврат JobHandle JobHandle_tDA498A2E49AEDE014468F416A8A98A6B258D73D1 L_1 = V_0; return L_1; }}

JobsUtility.Schedule_Injected в том же самом файле:

IL2CPP_EXTERN_C IL2CPP_METHOD_ATTR void JobsUtility_Schedule_Injected_mC918219A2045C68697BD2C7FCE7DCA515CE09C01 ( JobScheduleParameters_t55DC8564D72859191CE4E81639EFD25F6C6A698F * ___parameters0, JobHandle_tDA498A2E49AEDE014468F416A8A98A6B258D73D1 * ___ret1, const RuntimeMethod* method){ // Определение типа указателя на функцию для вызова typedef void (*JobsUtility_Schedule_Injected_mC918219A2045C68697BD2C7FCE7DCA515CE09C01_ftn) ( JobScheduleParameters_t55DC8564D72859191CE4E81639EFD25F6C6A698F *, JobHandle_tDA498A2E49AEDE014468F416A8A98A6B258D73D1 *); // Создание одиночного указателя для всех вызовов этой функции static JobsUtility_Schedule_Injected_mC918219A2045C68697BD2C7FCE7DCA515CE09C01_ftn _il2cpp_icall_func; // Когда указатель на функцию не установлен(например, при первом вызове), вызвать // il2cpp_codegen_resolve_icall со строкой, представляющей вызов функции // и инициализировать указатель возвращаемым значением if (!_il2cpp_icall_func) _il2cpp_icall_func = (JobsUtility_Schedule_Injected_mC918219A2045C68697BD2C7FCE7DCA515CE09C01_ftn)il2cpp_codegen_resolve_icall ( "Unity.Jobs.LowLevel.Unsafe.JobsUtility::Schedule_Injected(Unity.Jobs.LowLevel.Unsafe.JobsUtility/JobScheduleParameters&,Unity.Jobs.JobHandle&)"); // Вызов функции по указателю, который вернула il2cpp_codegen_resolve_icall // с аргументами JobScheduleParameters и JobHandle _il2cpp_icall_func(___parameters0, ___ret1);}
il2cpp/libil2cpp/codegen/il2cpp-codegen-il2cpp.cpp

Теперь мы делаем вызов в код движка Unity, а не в код, сгенерированный IL2CPP. Чтобы в этом убедиться, откройте указанный файл в папке с установленным Unity. Теперь немного удобнее читать код т.к. он написан вручную в Unity.

Il2CppMethodPointer il2cpp_codegen_resolve_icall(const char* name){ // Получить указатель на функцию через имя функции Il2CppMethodPointer method = il2cpp::vm::InternalCalls::Resolve(name); // Если мы не можем получить указатель, бросить исключение if (!method) { il2cpp::vm::Exception::Raise(il2cpp::vm::Exception::GetMissingMethodException(name)); } // Вернуть указатель return method;}
il2cpp/libil2cpp/vm/InternalCalls.cpp

И в последнюю очередь взглянем на il2cpp::vm::InternalCalls::Resolve, чтобы узнать, как возвращается указатель. Эта функция уже имеет комментарии, объясняющие свою работу, поэтому я воздержусь и не буду добавлять что-либо.

Il2CppMethodPointer InternalCalls::Resolve(const char* name){ // Для начала пробуем найти полное имя, если аргументы переданы, // потом ищем используя type::method // Пример: First, System.Foo::Bar(System.Int32) // потом, System.Foo::Bar ICallMap::iterator res = s_InternalCalls.find(name); if (res != s_InternalCalls.end()) return res->second; std::string shortName(name); size_t index = shortName.find('('); if (index != std::string::npos) { shortName = shortName.substr(0, index); res = s_InternalCalls.find(shortName); if (res != s_InternalCalls.end()) return res->second; } return NULL;}

s_InternalCalls объявлена в начале файла как глобальная переменная. Это просто словарь, который действует как кэш, и содержит имена функций с указателями на них.

typedef std::map<std::string, Il2CppMethodPointer> ICallMap;static ICallMap s_InternalCalls;

Этот словарь заполняется с помощью другой функции из того же файла:

void InternalCalls::Add(const char* name, Il2CppMethodPointer method){ //ICallMap::iterator res = s_InternalCalls.find(name); // TODO: Не вызывать Assert прямо сейчас, потому что Unity добавляет icalls несколько раз. //if (res != icalls.end()) // IL2CPP_ASSERT(0 && "Adding internal call twice!"); IL2CPP_ASSERT(method); s_InternalCalls[name] = method;}
il2cpp/libil2cpp/il2cpp-api.cpp

Все вызовы InternalCalls::Add идут через простую сквозную функцию, которая является частью IL2CPP:

void il2cpp_add_internal_call(const char* name, Il2CppMethodPointer method){ return InternalCalls::Add(name, method);}
Помимо исходного кода

На данный момент мы прошлись по всему коду, который находит и вызывает скомпилированную через Burst функцию. Оставшаяся часть головоломки – зайти в папку с билдом игры для macOS и посмотреть бинарный файл, скомпилированный Burst. Мы можем найти его здесь:

Contents/|--Plugins/ |--lib_burst_generated.bundle |--lib_burst_generated.txt

Откройте lib_burst_generated.txt, который вносит некоторую ясность:

--platform=macOS--backend=burst-llvm--target=X64_SSE4--noalias--dump=Function--float-precision=Standard--output=/path/to/project/Temp/StagingArea/UnityPlayer.app/Contents/Plugins/lib_burst_generated--method=System.Void Unity.Jobs.IJobExtensions/JobStruct`1<Job>::Execute(T&,System.IntPtr,System.IntPtr,Unity.Jobs.LowLevel.Unsafe.JobRanges&,System.Int32)--a3a2767d0992906d22747c995417de24--method=System.Void Unity.Jobs.IJobParallelForExtensions/ParallelForJobStruct`1<ParallelJob>::Execute(T&,System.IntPtr,System.IntPtr,Unity.Jobs.LowLevel.Unsafe.JobRanges&,System.Int32)--e822743a36a5dcf3c62a16e009c55d24--platform=macOS--backend=burst-llvm--target=X64_SSE2--noalias--dump=Function--float-precision=Standard--output=/path/to/project/Temp/StagingArea/UnityPlayer.app/Contents/Plugins/lib_burst_generated--method=System.Void Unity.Jobs.IJobExtensions/JobStruct`1<Job>::Execute(T&,System.IntPtr,System.IntPtr,Unity.Jobs.LowLevel.Unsafe.JobRanges&,System.Int32)--a3a2767d0992906d22747c995417de24--method=System.Void Unity.Jobs.IJobParallelForExtensions/ParallelForJobStruct`1<ParallelJob>::Execute(T&,System.IntPtr,System.IntPtr,Unity.Jobs.LowLevel.Unsafe.JobRanges&,System.Int32)--e822743a36a5dcf3c62a16e009c55d24

Тут мы видим, что Burst компилирует наш код дважды: один раз для SSE4 и один для SSE2. Это делает игру совместимой с более широким набором процессоров, поскольку macOS будет загружать подходящую для процессора библиотеку из lib_burst_generated.bundle при запуске игры.

Conclusion

IL2CPP работает независимо от Burst. Это происходит через вызовы в скомпилированный Burst код, очень похожим образом происходит вызов в любой другой нативный код через P/Invoke. Это имеет смысл потому, что, как минимум, в macOS, Burst компилирует код в динамическую библиотеку и прямые вызовы невозможны.

Эта интеграция на уровне ОС идет с накладными расходами. Все вызовы не бесплатны и идут с некоторыми затратами, особенно при первом вызове. Это может привести к падению производительности, если этим злоупотреблять и использовать Burst для слишком малого объема работ. Тем не менее, стоимость относительно низкая, поэтому нет необходимости использовать Burst только для самых тяжелых задач. Это отличные новости!

JacksonDunstan.com |   Как IL2CPP вызывает Burst (2024)

References

Top Articles
Latest Posts
Recommended Articles
Article information

Author: Terence Hammes MD

Last Updated:

Views: 5763

Rating: 4.9 / 5 (69 voted)

Reviews: 92% of readers found this page helpful

Author information

Name: Terence Hammes MD

Birthday: 1992-04-11

Address: Suite 408 9446 Mercy Mews, West Roxie, CT 04904

Phone: +50312511349175

Job: Product Consulting Liaison

Hobby: Jogging, Motor sports, Nordic skating, Jigsaw puzzles, Bird watching, Nordic skating, Sculpting

Introduction: My name is Terence Hammes MD, I am a inexpensive, energetic, jolly, faithful, cheerful, proud, rich person who loves writing and wants to share my knowledge and understanding with you.