Il2Cpp: Общая Реализация

В предыдущей статье серии о IL2CPP Мы рассмотрели вызовы методов в сгенерированном коде C++.

Сейчас мы поговорим об одной из важнейших особенностей кода IL2CPP — обобщенной реализации методов, позволяющей существенно уменьшить размер исполняемого файла IL2CPP. Стоит отметить, что универсальная реализация также используется в средах выполнения Mono и .

NET. Изначально он не поддерживался в IL2CPP и был добавлен только со временем.



IL2CPP: общая реализация

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

Имейте в виду, что код, сгенерированный в этой статье, может измениться в будущих версиях Unity. Но, как правило, мы обсуждаем такие изменения сразу после их выхода.



Что такое общая реализация?

Представьте, что вам нужно написать реализацию класса List на C#.

Будет ли эта реализация зависеть от типа T? Можно ли использовать реализацию метода Add как для строки списка, так и для объекта списка? А как насчет списка DateTime? На самом деле, дженерики хороши тем, что их реализации на C# подходят для совместного использования, а это значит, что обобщенный класс List подходит для любого типа T. Но что, если List необходимо преобразовать из C# во что-то исполняемое, например, в ассемблер? код, такой как Mono, или код C++ в случае IL2CPP? Можем ли мы затем использовать ту же реализацию метода Add? В большинстве случаев да.

Как мы увидим далее в этой статье, возможность универсальной реализации почти полностью зависит от размера типа T. Если это ссылочный тип (строка или объект), его размер всегда равен размеру указателя.

.

Если T — тип значения (int или DateTime), его размер может варьироваться, и это немного усложняет ситуацию.

В конечном счете, чем больше методов имеют общую реализацию, тем меньше размер исполняемого кода.

Марк Пробст, разработчик, реализовавший обобщенную реализацию в Mono, написал об этом несколько интересных статей.

Мы не будем слишком глубоко углубляться в само понятие, а поговорим о том, как и при каких условиях оно используется в IL2CPP. Я думаю, эта информация может дать вам более полное представление о размере исполняемого файла вашего проекта.



Особенности обобщенной реализации в IL2CPP

IL2CPP поддерживает универсальную реализацию методов для типа SomeGenericType, если T является ссылочным типом (строка, объект или любой пользовательский класс), целочисленным типом или перечислением.

Для типов значений универсальная реализация не поддерживается, поскольку их размер может варьироваться в зависимости от размера полей.

Это означает, что добавление SomeGenericType, где T — ссылочный тип, практически не повлияет на размер исполняемого файла.

С другой стороны, если T является типом значения, последствия будут более заметными.

Это работает одинаково в Mono и IL2CPP. Но давайте перейдем непосредственно к деталям реализации.



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

Я буду использовать Unity версии 5.0.2p1 для Windows для сборки проекта для WebGL. При этом я включу параметр «Проигрыватель разработки» и установлю для параметра «Включить исключения» значение «Нет».

Для начала напишем метод драйвера для создания экземпляров универсальных типов, которые мы и рассмотрим:

  
  
  
  
  
  
   

public void DemonstrateGenericSharing() { var usesAString = new GenericType<string>(); var usesAClass = new GenericType<AnyClass>(); var usesAValueType = new GenericType<DateTime>(); var interfaceConstrainedType = new InterfaceConstrainedGenericType<ExperimentWithInterface>(); }

Далее мы определяем типы, используемые в этом методе:

class GenericType<T> { public T UsesGenericParameter(T value) { return value; } public void DoesNotUseGenericParameter() {} public U UsesDifferentGenericParameter<U>(U value) { return value; } } class AnyClass {} interface AnswerFinderInterface { int ComputeAnswer(); } class ExperimentWithInterface : AnswerFinderInterface { public int ComputeAnswer() { return 42; } } class InterfaceConstrainedGenericType<T> where T : AnswerFinderInterface { public int FindTheAnswer(T experiment) { return experiment.ComputeAnswer(); } }

Весь код вложен в класс HelloWorld, который является производным от MonoBehaviour. Вы также можете заметить, что командная строка il2cpp.exe больше не содержит параметр -enable-generic-sharing, поскольку
в первой статье этой серии .

Однако генерализованная реализация происходит, но теперь автоматически.



Обобщенная реализация ссылочных типов

Для начала давайте рассмотрим самый распространенный случай — ссылочные типы.

В управляемом коде эти типы являются производными от System.Object, а в сгенерированном коде — от Object_t. Поэтому для их представления в коде C++ можно использовать заполнитель Object_t*.

Давайте найдем сгенерированную версию метода DemonstrateGenericSharing. В моем проекте он называется HelloWorld_DemonstrateGenericSharing_m4. Нас интересуют определения четырех методов класса GenericType. С помощью Ctags мы можем перейти к объявлению метода GenericType_1__ctor_m8 (конструктор GenericType).

Обратите внимание, что это объявление метода представляет собой оператор #define, сопоставляющий этот метод с методом GenericType_1__ctor_m10447_gshared. Теперь давайте найдем объявления методов для GenericType. Интересно, что объявление конструктора GenericType_1__ctor_m9 также является оператором #define, связанным с той же функцией — GenericType_1__ctor_m10447_gshared! Комментарий к коду определения GenericType_1__ctor_m10447_gshared указывает, что этот метод соответствует имени управляемого метода HelloWorld/GenericType`1. ::.

ктор().

Это конструктор объектного типа GenericType, который называется полностью универсальным — если взять тип GenericType, то для любого ссылочного типа T реализация всех методов будет использовать версию, где T — объект. Чуть ниже конструктора в сгенерированном коде вы можете увидеть метод UsesGenericParameter:

extern "C" Object_t * GenericType_1_UsesGenericParameter_m10449_gshared (GenericType_1_t2159 * __this, Object_t * ___value, MethodInfo* method) { { Object_t * L_0 = ___value; return L_0; } }

В обоих случаях, когда встречается общий параметр T (тип возвращаемого значения и тип отдельного аргумента), сгенерированный код использует тип Object_t*.

А учитывая, что все ссылочные типы в таком коде могут быть представлены через Object_t*, эту реализацию метода можно вызвать для любого T, являющегося ссылочным типом.

Во второй статье в этой серии статей я упоминал, что все определения методов в C++ являются свободными функциями.

Утилита il2cpp.exe не использует наследование C++ для создания переопределенных методов C#, но использует его для типов.

Выполнив поиск по запросу «AnyClass_t», мы увидим, как выглядит тип C# AnyClass в C++:

struct AnyClass_t1 : public Object_t { };

Учитывая, что AnyClass_t1 является производным от Object_t, мы можем просто передать ему указатель в качестве аргумента функции GenericType_1_UsesGenericParameter_m10449_gshared. Но как насчет возвращаемого значения? Мы не можем вернуть указатель на базовый класс там, где ожидается указатель на производный класс, не так ли? Взгляните на объявление метода GenericType::UsesGenericParameter:

#define GenericType_1_UsesGenericParameter_m10452(__this, ___value, method) (( AnyClass_t1 * (*) (GenericType_1_t6 *, AnyClass_t1 *, MethodInfo*))GenericType_1_UsesGenericParameter_m10449_gshared)(__this, ___value, method)

В сгенерированном коде возвращаемое значение (тип Object_t*) фактически становится производным типом AnyClass_t1*.

Оказывается, IL2CPP обманывает компилятор C++, чтобы избежать системы типов C++.



Обобщенная реализация с ограничениями

Допустим, нам нужно разрешить вызов некоторых методов объекта типа T, но не помешает ли этому использование Object_t*? Так и будет, но сначала нам нужно передать эту идею компилятору C#, используя общие ограничения.

Взгляните еще раз на код сценария, а именно на InterfaceConstrainedGenericType. Этот универсальный тип использует предложениеwhere, чтобы сделать тип T производным от интерфейса AnswerFinderInterface, тем самым позволяя вызывать метод ComputeAnswer. В предыдущей статье мы говорили о том, что вызов методов интерфейса требует поиска в vtable. А поскольку метод FindTheAnswer выполняет прямой вызов функции для экземпляра ограниченного типа T (представленного Object_t*), код C++ может использовать полностью универсальную реализацию.

Переходя от реализации функции HelloWorld_DemonstrateGenericSharing_m4 к определению функции InterfaceConstrainedGenericType_1__ctor_m11, мы видим, что этот метод также является оператором #define, связанным с функцией InterfaceConstrainedGenericType_1__ctor_m10456_gshared. Чуть ниже представлена реализация функции InterfaceConstrainedGenericType_1_FindTheAnswer_m10458_gshared, которая принимает аргумент Object_t* и также является полностью универсальной.

Вызов InterfaceFuncInvoker0::Invoke позволяет выполнить вызов управляемого метода ComputeAnswer.

extern "C" int32_t InterfaceConstrainedGenericType_1_FindTheAnswer_m10458_gshared (InterfaceConstrainedGenericType_1_t2160 * __this, Object_t * ___experiment, MethodInfo* method) { static bool s_Il2CppMethodIntialized; if (!s_Il2CppMethodIntialized) { AnswerFinderInterface_t11_il2cpp_TypeInfo_var = il2cpp_codegen_class_from_type(&AnswerFinderInterface_t11_0_0_0); s_Il2CppMethodIntialized = true; } { int32_t L_0 = (int32_t)InterfaceFuncInvoker0<int32_t>::Invoke(0 /* System.Int32 HelloWorld/AnswerFinderInterface::ComputeAnswer() */, AnswerFinderInterface_t11_il2cpp_TypeInfo_var, (Object_t *)(*(&amp;___experiment))); return L_0; } }

Важно помнить, что IL2CPP рассматривает любой управляемый интерфейс как System.Object. Это правило применяется к любому коду, созданному утилитой il2cpp.exe.

Ограничения базового класса

Помимо ограничений интерфейса, C# допускает ограничения базового класса.

Но если IL2CPP не рассматривает базовые классы как System.Object, как работает универсальная реализация? Поскольку базовые классы всегда являются ссылочными типами, IL2CPP использует для них полностью универсальные методы.

Любой код, использующий поле или вызывающий метод ограниченного типа, подвергается приведению типа в C++.

Опять же, компилятор C# обеспечивает правильную реализацию обобщенного ограничения, а мы обманываем компилятор C++ относительно типа.



Общая реализация типов значений

Давайте вернемся к функции HelloWorld_DemonstrateGenericSharing_m4 и посмотрим на реализацию GenericType. DateTime — это ссылочный тип, поэтому GenericType не является универсальным.

Перейдем к объявлению конструктора этого типа GenericType_1__ctor_m10. Здесь, как и в других случаях, мы видим #define, но он связан с функцией GenericType_1__ctor_m10_gshared, используемой только одним классом — GenericType.

Концептуализация общей реализации

Понятие общей реализации может быть довольно трудным для понимания.

В предметной области полно патологических случаев (одни и те же рекурсивные закономерности).

Поэтому здесь необходимо выделить несколько основных принципов:

  • Реализация любого метода универсального типа является универсальной.

  • В некоторых случаях реализация методов является универсальной только для определенного типа (например, приведенного выше типа с параметром универсального типа значения GenericType).

  • Типы с параметром универсального ссылочного типа используют полностью универсальную реализацию, рассматривая параметры всех типов как System.Object.
  • Типы с двумя или более типами параметров могут быть частично универсальными, если хотя бы один из типов параметров является ссылочным типом.

Для любого универсального типа il2cpp.exe всегда создает полностью универсальные реализации метода.

Другие реализации генерируются только при необходимости.



Обобщенные методы

Универсальная реализация используется не только для универсальных типов, но и для универсальных методов.

Обратите внимание, что в исходном коде сценария метод UsesDifferentGenericParameter использует другой тип параметра, чем класс GenericType. Но когда мы посмотрели на универсальную реализацию класса GenericType, мы не увидели этого метода.

Выполнив поиск по «UsesDifferentGenericParameter», мы видим, что реализация этого метода находится в файле GenericMethods0.cpp:

extern "C" Object_t * GenericType_1_UsesDifferentGenericParameter_TisObject_t_m15243_gshared (GenericType_1_t2159 * __this, Object_t * ___value, MethodInfo* method) { { Object_t * L_0 = ___value; return L_0; } }

Это полностью универсальная реализация, принимающая тип Object_t*.

И хотя этот метод является универсальным типом, поведение неуниверсального метода будет таким же.

Можно утверждать, что il2cpp.exe всегда пытается сгенерировать минимальный объем кода для реализации методов с универсальными параметрами.



Заключение

Универсальная реализация — одно из наиболее важных улучшений IL2CPP с момента его выпуска, позволяющее значительно уменьшить размер кода C++ для реализации методов с одинаковым поведением.

Мы продолжаем искать решения по уменьшению размера двоичных файлов и пытаемся воспользоваться преимуществами и возможностями общей реализации.

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

Теги: #c++ #il2cpp #общее совместное использование #программирование #.

NET #C++ #Разработка игр #unity

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.