Как IL2CPP вызывает Burst
(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 void IJobExtensions_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_tDA498A2E49AEDE014468F416A8A98A6B258D73D1 JobsUtility_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 только Ð´Ð»Ñ Ñамых Ñ‚Ñжелых задач. Ðто отличные новоÑти!