Windows build support il2cpp что это

IL2CPP: экскурсия по генерируемому коду

Это второй пост из серии по IL2CPP. В этой статье мы будем исследовать C++ код, сгенерированный il2cpp.exe. Мы увидим, как представлены управляемые типы в машинном коде, посмотрим на проверки во время выполнения, используемые для поддержки виртуальной машины .NET, как генерируются циклы и многое другое!

Мы получим очень версия-зависимый код, который, безусловно, будет меняться в более поздних версиях Unity. Тем не менее, концепции останутся такими же.

Пример проекта

Для этого примера я буду использовать последнюю доступную версию Unity, 5.0.1p1. Как и в первом посте этой серии, я начну с пустого проекта и добавлю один скрипт со следующим содержанием:

[csharp]
using UnityEngine;

public class HelloWorld : MonoBehaviour <
private class Important <
public static int ClassIdentifier = 42;
public int InstanceIdentifier;
>

void Start () <
Debug.Log(«Hello, IL2CPP!»);

Debug.LogFormat(«Static field: <0>«, Important.ClassIdentifier);

Debug.LogFormat(«First value: <0>«, importantData[0].InstanceIdentifier);
Debug.LogFormat(«Second value: <0>«, importantData[1].InstanceIdentifier);
try <
throw new InvalidOperationException(«Don’t panic»);
>
catch (InvalidOperationException e) <
Debug.Log(e.Message);
>

for (var i = 0; i ___x_1);
float L_2 = L_1;
// Box the float instance, since it is a value type.
Object_t * L_3 = Box(InitializedTypeInfo(&Single_t264_il2cpp_TypeInfo), &L_2);
// Here are three important runtime checks
NullCheck(L_0);
IL2CPP_ARRAY_BOUNDS_CHECK(L_0, 0);
ArrayElementTypeCheck (L_0, L_3);
// Store the boxed value in the array at index 0
*((Object_t **)(Object_t **)SZArrayLdElema(L_0, 0)) = (Object_t *)L_3;
[/cpp]

Три проверки отсутствуют в коде IL, но добавляются il2cpp.exe.

  • Проверка NullCheck бросит исключение NullReferenceException, если значение массива равно null.
  • IL2CPP_ARRAY_BOUNDS_CHECK бросит исключение IndexOutOfRangeException, если индекс массива не является правильным.
  • ArrayElementTypeCheck бросит исключение ArrayTypeMismatchException, если тип элемента, добавленного в массив, не является правильным.

Эти три проверки гарантируют правильность данных для виртуальной машины .NET. Вместо внедрения кода, Mono использует механизмы целевой платформы для обработки этих же проверок во время выполнения. Мы хотели, чтобы IL2CPP мог охватить больше платформ, включая такие как WebGL, где нет своего механизма проверок, поэтому il2cpp.exe вводит эти проверки.

Эти проверки создают проблемы с производительностью? В большинстве случаев, мы не видели какого-либо ухудшения производительности, к тому же они обеспечивают преимущества и безопасность, которые требуются в виртуальной машине .NET. Однако, в некоторых отдельных случаях мы заметили, что эти проверки привели к снижению производительности, особенно в труднодоступных циклах. Мы работаем над способом, который позволит управляемому коду удалить эти динамические проверки, когда il2cpp.exe генерирует C++ код. Следите за обновлениями.

Статические поля

Теперь, когда мы увидели, как выглядят поля экземпляра (на примере Vector3), давайте посмотрим, как преобразуются статические поля и как организован доступ к ним. Найдем определение метода HelloWorld_Start_m3, который находится в файле Bulk_Assembly-CSharp_0.cpp в моей сборке. Оттуда переходим к типу Important_t1 (в файле theAssemblyU2DCSharp_HelloWorld_Important.h):

[cpp]
struct Important_t1 : public Object_t
<
// System.Int32 HelloWorld/Important::InstanceIdentifier
int32_t ___InstanceIdentifier_1;
>;
struct Important_t1_StaticFields
<
// System.Int32 HelloWorld/Important::ClassIdentifier
int32_t ___ClassIdentifier_0;
>;
[/cpp]

Обратите внимание, что il2cpp.exe создала отдельную С++ структуру, чтобы предоставить статическое поле для этого типа, так как оно должно быть доступно всем экземплярам этого типа. Таким образом, во время выполнения, будет создан один экземпляр типа Important_t1_StaticFields, и все экземпляры типа Important_t1 будут использовать этот экземпляр как статическое поле. В сгенерированном коде доступ к статическому полю происходит следующим образом:

Читайте также:  Глад валакас windows 10 обои

Метаданные типа для Important_t1 содержит указатель на один экземпляр типа Important_t1_StaticFields, и информацию о том, что этот экземпляр используется для получения значения статического поля.

Исключения

Управляемые исключения преобразуются il2cpp.exe в C++ исключения. Мы выбрали такой подход, чтобы избежать зависимости от платформы. Когда il2cpp.exe нужно создавать код, способный бросить управляемое исключение, он вызывает функцию il2cpp_codegen_raise_exception.

Блок try…catch для управляемых исключений в нашем методе HelloWorld_Start_m3 выглядит так:

[cpp]
try
< // begin try (depth: 1)
InvalidOperationException_t7 * L_17 = (InvalidOperationException_t7 *)il2cpp_codegen_object_new (InitializedTypeInfo(&InvalidOperationException_t7_il2cpp_TypeInfo));
InvalidOperationException__ctor_m8(L_17, (String_t*) &_stringLiteral5, /*hidden argument*/&InvalidOperationException__ctor_m8_MethodInfo);
il2cpp_codegen_raise_exception(L_17);
// IL_0092: leave IL_00a8
goto IL_00a8;
> // end try (depth: 1)
catch(Il2CppExceptionWrapper& e)
<
__exception_local = (Exception_t8 *)e.ex;
if(il2cpp_codegen_class_is_assignable_from (&InvalidOperationException_t7_il2cpp_TypeInfo, e.ex->object.klass))
goto IL_0097;
throw e;
>
IL_0097:
< // begin catch(System.InvalidOperationException)
V_1 = ((InvalidOperationException_t7 *)__exception_local);
NullCheck(V_1);
String_t* L_18 = (String_t*)VirtFuncInvoker0 ::Invoke(&Exception_get_Message_m9_MethodInfo, V_1);
Debug_Log_m6(NULL /*static, unused*/, L_18, /*hidden argument*/&Debug_Log_m6_MethodInfo);
// IL_00a3: leave IL_00a8
goto IL_00a8;
> // end catch (depth: 1)
[/cpp]

Все управляемые исключения заворачиваются в Il2CppExceptionWrapper. Когда сгенерированный код ловит исключение этого типа, он распаковывает C++ представление управляемого исключения (которое имеет тип Exception_t8). В данном случае, мы ищем только InvalidOperationException, поэтому если мы не найдем исключение этого типа, C++ снова бросит это исключение. Если же мы встречаем исключение правильного типа, то код переходит к реализации обработчика, и выводит сообщение исключения.

Goto.

Этот код вызывает интересный вопрос. Что эти ярлыки и goto там делают? Эти конструкции не являются необходимыми в структурном программировании! Тем не менее, IL не имеет концепции структурированного программирования, таких как циклы, конструкции if/then. Так как этот код низкоуровневый, il2cpp.exe придерживается концепции низкоуровневого программирования в сгенерированном коде.

Для примера, давайте посмотрим на цикл в методе HelloWorld_Start_m3:

[cpp]
IL_00a8:
<
V_2 = 0;
goto IL_00cc;
>
IL_00af:
<
ObjectU5BU5D_t4* L_19 = ((ObjectU5BU5D_t4*)SZArrayNew(ObjectU5BU5D_t4_il2cpp_TypeInfo_var, 1));
int32_t L_20 = V_2;
Object_t * L_21 =
Box(InitializedTypeInfo(&Int32_t5_il2cpp_TypeInfo), &L_20);
NullCheck(L_19);
IL2CPP_ARRAY_BOUNDS_CHECK(L_19, 0);
ArrayElementTypeCheck (L_19, L_21);
*((Object_t **)(Object_t **)SZArrayLdElema(L_19, 0)) = (Object_t *)L_21;
Debug_LogFormat_m7(NULL /*static, unused*/, (String_t*) &_stringLiteral6, L_19, /*hidden argument*/&Debug_LogFormat_m7_MethodInfo);
V_2 = ((int32_t)(V_2+1));
>
IL_00cc:
<
if ((((int32_t)V_2) ___x_1);
float L_2 = L_1;
[/cpp]

Очевидно, что здесь не требуется переменная L_2. Большинство компиляторов C++ может оптимизировать эту задачу, но мы хотели бы вообще избежать ее создания. В настоящее время мы исследуем возможности использования AST, чтобы лучше понять код IL и генерировать лучший C++ код для случаев, использующих локальные переменные и циклов.

Вывод

Мы рассмотрели только малую часть C++ кода, сгенерированного IL2CPP для очень простого проекта. Если вы этого не сделали, я призываю вас погрузиться в сгенерированный код вашего проекта. Изучая его, имейте в виду, что генерируемый C++ код будет выглядеть по-другому в будущих версиях Unity, так как мы постоянно работаем над улучшением качества и производительности IL2CPP.

Путем преобразования IL кода в C++, мы добились хорошего баланса между портативностью и производительностью кода. Мы смогли получить много полезных для разработчиков функций управляемого кода, и в то же время по-прежнему пользуемся преимуществами машинного кода, которые обеспечивает компилятор С++ для различных платформ.

В будущих постах, мы будем изучать больше сгенерированного кода, в том числе вызовы методов, распределения реализаций методов и обертки для вызова нативных библиотек. Но в следующий раз мы будем отлаживать некоторую часть из сгенерированного кода для 64-битной сборки iOS, используя Xcode.

Читайте также:  Wunderlist для windows 10

IL2CPP: вызовы методов

Это четвертая статья из серии по IL2CPP. В ней мы поговорим о том, как il2cpp.exe генерирует код C++ для вызовов методов в управляемом коде.

В частности, мы рассмотрим шесть типов вызовов:

  • прямые вызовы методов экземпляра и статических методов;
  • вызовы через делегат времени компиляции;
  • вызовы через виртуальный метод;
  • вызовы через метод интерфейса;
  • вызовы через делегат времени выполнения;
  • вызовы через рефлексию.

Мы обратим внимание на действия генерируемого кода C++, а также на затраты, связанные с каждым типом вызовов. Как я уже говорил, представленный код наверняка изменится в следующих версиях Unity. Но основные принципы останутся неизменными.

Предыдущие статьи из серии:

Подготовка к работе

Я буду использовать версию Unity 5.0.1p4 на Windows для сборки проекта под WebGL. При этом я включу опцию Development Player и задам значение Full для Enable Exceptions. Чтобы проанализировать различные типы вызовов методов, я буду использовать модифицированный скрипт из предыдущей статьи, начинающийся с интерфейса и определения класса:

За ними следуют константное поле и тип делегата:

Наконец, мы указываем интересующие нас методы, а также обязательный метод Start (в нашем случае пустой):

Итак, всё готово. Учтите, что, пока открыт редактор, генерируемый код C++ находится в директории Temp\StagingArea\Data\il2cppOutput. И не забудьте сгенерировать файл тегов с помощью Ctags, чтобы упростить навигацию по коду.

Прямые вызовы

Вызвать метод проще и, как вы сможете убедиться, быстрее всего напрямую. Вот так выглядит генерируемый код для метода CallDirectly:

Последняя строка и есть вызов метода. Обратите внимание, что она всего лишь вызывает свободную функцию, заданную в коде C++. Как мы уже говорили в предыдущей статье, IL2CPP не использует функции-члены или виртуальные функции, а генерирует все методы как свободные функции C++. Аналогичным образом работает прямой вызов статического метода. Вот так выглядит генерируемый код для метода CallStaticMethodDirectly:

Можно сказать, что вызывать статический метод менее затратно, поскольку нам не нужно создавать и инициализировать экземпляр объекта. Но сам вызов метода – суть один и тот же. Единственное отличие в том, что для первого аргумента статических функций IL2CPP всегда передает значение NULL. Учитывая, что разница между вызовами статических методов и методов экземпляра настолько мала, в рамках этой статьи мы их отождествим.

Вызовы через делегат времени компиляции

У непрямого вызова через делегат есть своя специфика. Сперва уточню, что я имею в виду под делегатом времени компиляции – уже во время компиляции мы знаем, какой метод вызывается из какого экземпляра объекта. Код для этого типа находится в методе CallViaDelegate. В генерируемом коде он выглядит так:

Обратите внимание, что вызываемый метод фактически не является частью генерируемого кода. Метод VirtFuncInvoker1 ::Invoke находится в файле GeneratedVirtualInvokers.h, генерируемом утилитой il2cpp.exe на основе использования виртуальных функций, которые возвращают значение (VirtFuncInvokerN) и не возвращают его (VirtActionInvokerN), где N означает количество аргументов. Вот так выглядит метод Invoke:

Вызов GetVirtualInvokeData ищет виртуальный метод в таблице vtable, генерируемой на основе управляемого кода, и затем вызывает этот метод.

Почему мы не использовали вариативные шаблоны C++ 11 для реализации методов VirtFuncInvokerN? Всё указывает на то, что в этом случае они пришлись бы как нельзя кстати. Однако для работы с кодом C++, генерируемым il2cpp.exe, нам понадобятся компиляторы C++, которые пока поддерживают не все аспекты C++ 11. Поэтому мы решили, что создание отдельной ветки генерируемого кода для компиляторов только усложнит процесс, и не стали этого делать.

Читайте также:  Process virtual memory windows

Но почему это именно виртуальный вызов метода? Разве мы не вызываем метод экземпляра в коде C#? Не забывайте, что мы делаем это через делегат C#. Взгляните еще раз на генерируемый код. Вызываемый метод передается через аргумент MethodInfo* (метаданные метода) – ImportantMethodDelegate_Invoke_m5_MethodInfo. Найдите метод под названием ImportantMethodDelegate_Invoke_m5 в генерируемом коде, и вы увидите, что вызов идет к управляемому методу Invoke типа ImportantMethodDelegate. Это виртуальный метод, и значит мы должны совершить виртуальный вызов: функция ImportantMethodDelegate_Invoke_m5 вызовет метод с именем Method в коде C#.

Итак, за счет небольшого изменения в коде C# мы перешли от одного вызова свободной функции C++ к нескольким вызовам, включая поиск по таблице. Тем не менее, вызывать метод через делегат намного затратнее, чем напрямую. Кстати, в процессе рассмотрения этого типа вызовов мы заодно поговорили о том, как работают вызовы через виртуальный метод.

Вызовы через интерфейс

Вызвать метод в C# также можно через интерфейс. Il2cpp.exe совершает такие вызовы по аналогии с вызовами виртуальных методов:

Обратите внимание, что вызов метода осуществляется через функцию InterfaceFuncInvoker1::Invoke в файле GeneratedInterfaceInvokers.h. Как и VirtFuncInvoker1, класс InterfaceFuncInvoker1 совершает поиск в таблице vtable с помощью функции il2cpp::vm::Runtime::GetInterfaceInvokeData в libil2cpp.

Почему вызовы через метод интерфейса и вызовы через виртуальный метод используют разные API в libil2cpp? Обращение к функции InterfaceFuncInvoker1::Invoke передает не только вызываемый метод и его аргументы, но и интерфейс (в этом случае L_1). Для каждого типа сохраняется таблица vtable, чтобы методы интерфейса фиксировались при смещении. Таким образом, il2cpp.exe должен предоставить интерфейс, чтобы определить, какой метод вызвать. В сухом остатке можно сказать, что вызовы через виртуальный метод и вызовы через интерфейс одинаково затратны в IL2CPP.

Вызовы через делегат времени выполнения

Делегат также можно создать во время выполнения с помощью метода Delegate.CreateDelegate. Это похоже на создание делегата во время компиляции, но требует вызова еще одной функции. Генерируемый код выглядит так:

Для создания и инициализации такого делегата требуется намного больше кода. Да и сам вызов метода получается более затратным. Во-первых, нужно создать массив для аргументов метода. Затем – вызвать метод DynamicInvoke из экземпляра Delegate. Обратите внимание, что этот метод вызывает функцию VirtFuncInvoker1::Invoke – так же, как и делегат времени компиляции. Таким образом, для делегата времени выполнения требуется не только еще один вызов функции, но и дополнительный поиск по таблице vtable.

Вызовы через рефлексию

Неудивительно, что самый затратный тип вызова метода – через рефлексию. Вот так выглядит генерируемый код для метода CallViaReflection:

Как и в случае делегата времени выполнения, нам нужно создать массив для аргументов метода. Затем мы совершаем вызов виртуального метода MethodBase::Invoke – функции MethodBase_Invoke_m24, которая, в свою очередь, вызывает другую виртуальную функцию. И только тогда осуществляется требуемый вызов метода.

Вывод

Хоть это и не сравнится с профилированием, разбор генерируемого кода C++ позволяет лучше понять затраты, связанные с тем или иным вызовом метода. Например, вызывать методы через делегат времени выполнения и через рефлексию лучше не стоит. Чтобы повысить производительность, измеряйте затраты на ранних этапах, желательно – профайлерами.

Оцените статью