Превью: IL2CPP Full Generic Sharing в Unity 2022.1 beta

JOSH PETERSON / UNITY TECHNOLOGIESSenior Software Engineer
Dec 15, 2021|12 Мин
Превью: IL2CPP Full Generic Sharing в Unity 2022.1 beta
Эта веб-страница была переведена с помощью машинного перевода для вашего удобства. Мы не можем гарантировать точность или надежность переведенного контента. Если у вас есть вопросы о точности переведенного контента, обращайтесь к официальной английской версии веб-страницы.

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

Что это такое?

Обобщения - это мощная особенность C#. Они позволяют коду выражать поведение независимо от типов. Как разработчики, мы ожидаем, что List<string> будет вести себя так же, как List<int> или List<T>, где T - любой тип.

В течение многих лет IL2CPP использовал общий доступ в случаях, когда T - это ссылочный тип (строка, объект и т.д.). Это хорошо работает, потому что ссылочные типы в C# всегда представлены указателем, поэтому размер и реализация List<string> будут соответствовать размеру и реализации List<object>. Но что произойдет, если T - это int (четыре байта) в 64-битной системе (где указатели - это восемь байт)? IL2CPP должен генерировать специальный код для List<int>, List<double>, List<MyValueType> и т.д.

Поэтому в Unity 2022.1 IL2CPP уже генерирует специальный код, который может обрабатывать List<T> для любого типа T, ссылки или значения. Эта технология называется Full Generic Sharing.

Какие проблемы он решает?

В то время как общие виртуальные методы являются выразительными функциями C#, которые хорошо работают при компиляции "точно в срок" (JIT), их трудно реализовать при компиляции "на опережение" (AOT), например, в IL2CPP. Вот тут-то и приходит на помощь Full Generic Sharing.

Давайте рассмотрим пример общего виртуального метода из руководства по Unity:

using UnityEngine;
using System;

public class AOTProblemExample : MonoBehaviour, IReceiver
{
    public enum AnyEnum 
    {
        Zero,
        One,
    }

    void Start() 
    {
        // Subtle trigger: The type of manager *must* be
        // IManager, not Manager, to trigger the AOT problem.
        IManager manager = new Manager();
        manager.SendMessage(this, AnyEnum.Zero);
    }

    public void OnMessage<T>(T value) 
    {
        Debug.LogFormat("Message value: {0}", value);
    }
}

public class Manager : IManager 
{
    public void SendMessage<T>(IReceiver target, T value) {
        target.OnMessage(value);
    }
}

public interface IReceiver
{
    void OnMessage<T>(T value);
}

public interface IManager 
{
    void SendMessage<T>(IReceiver target, T value);
}

Этот код демонстрирует выразительность общих виртуальных методов. Другими словами, мы можем отправлять данные любого типа ("сообщение") из любого класса, реализующего интерфейс IManager, в любой класс, реализующий интерфейс IReceiver. С IL2CPP в Unity 2021.2 этот, казалось бы, простой код не работает. Во время выполнения в журнале игрока появляется следующая ошибка:

ExecutionEngineException: Attempting to call method 'Test::OnMessage<Test+AnyEnum>' for which no ahead of time (AOT) code was generated. Consider increasing the --generic-virtual-method-iterations=1 argument

at Manager.SendMessage[T] (IReceiver target, T value) [0x00000] in <00000000000000000000000000000000>:0

at Test.Start () [0x00000] in <00000000000000000000000000000000>:0

Давайте разберемся с этой ошибкой.

Поскольку вызов Send Message в методе Start происходит через интерфейс (IManager, то есть "виртуальная" часть generic virtual), IL2CPP не определяет, какой метод будет вызван во время выполнения, когда код компилируется.

Вам может быть интересно: Почему IL2CPP не может разобраться с этим? Ну, это возможно! IL2CPP может перебрать весь код, доступный во время компиляции, и определить места, где этот вызов может оказаться. Но этот поиск стоит дорого; он отнимает драгоценное время, пока Вы ждете сборки проекта, и может заставить IL2CPP генерировать лишний код, который никогда не будет вызван, увеличивая конечный размер исполняемого файла.

Аргумент --generic-virtual-method-iterations (упомянутый в сообщении об ошибке) позволяет Вам указать IL2CPP, сколько времени он должен тратить на поиск. Для JIT-компилятора такой вызов общего виртуального метода очень прост. Он может "увидеть" целевой метод во время выполнения и сделать то, что нужно. В Unity 2022.1 IL2CPP научился тому же трюку. Теперь он генерирует новую, специальную версию SendMessage - полностью разделяемую версию.

Это будет работать независимо от типа T, ссылки или значения, а это значит, что если IL2CPP не видит, каким должен быть целевой метод во время компиляции, то вместо этого он вызовет эту полностью разделяемую версию. Код на C# не менее выразителен, работает во время выполнения и компилируется так же быстро.

Расскажите мне больше

Технология Full Generic Sharing невероятно полезна тем, что позволяет коду на платформах AOT вести себя гораздо более похоже на код на платформах JIT. Это приводит к меньшему количеству сюрпризов во время выполнения.

Оказывается, эти ошибки ExecutionEngineException проявляются и в других случаях. Когда IL2CPP не может определить, какой код запускать, может возникнуть эта ошибка. Мы часто видим это в сериализаторах, когда некоторые новые сериализованные данные десериализуются в тип, который IL2CPP не может определить. Но в Unity 2022.1 IL2CPP больше не выдает ExecutionEngineException, устраняя целый класс ошибок, которые трудно исправить.

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

Раньше IL2CPP выдавал следующую ошибку, когда Вашему коду во время выполнения требовались некоторые из этих глубоко вложенных типов: "IL2CPP столкнулся с управляемым типом, который он не может преобразовать заранее. Тип использует общие типы или типы массивов, вложенность которых превышает максимальную глубину, которую можно преобразовать." Сегодня Full Generic Sharing позволяет IL2CPP использовать реализацию, которая никогда не дает сбоев, поэтому Вы больше не столкнетесь с этим сообщением об ошибке.

Представьте, что у Вас есть проект, который Вы хотите изменить и сделать как можно меньше. Хотя у Вас может быть исполняемый код для List<int>, List<double> и List<string>, Вы, возможно, захотите переосмыслить балансировку такого количества различных реализаций.

Разве не здорово было бы иметь всего одну, полностью разделяемую общую реализацию для любого List<T>? Проверьте опцию IL2CPP Code Generation "Faster (smaller) builds" в Player Settings. Он использует Full Generic Sharing, чтобы предоставить Вам минимально возможный исполняемый код с максимально быстрым временем сборки - не говоря уже о быстрых инкрементальных сборках. Если Вы решите использовать List<DateTime> (или любой другой T) в проекте, IL2CPP больше не нужно генерировать или компилировать новый код для этой реализации.

Начало работы в бета-версии

Если Вы хотите начать писать код, использующий IL2CPP Full Generic Sharing, просто загрузите Unity 2022.1 beta из Unity Hub или с нашей страницы загрузки. Помните, что бета-версия не предназначена для использования в проектах на стадии производства, поэтому не забудьте сделать резервные копии существующих проектов.

Тем не менее, мы будем рады узнать, как Unity 2022.1 работает для Вас. Пожалуйста, посетите форум бета-версии, чтобы оставить нам свои мысли. Мы будем очень признательны за Ваши отзывы о Full Generic Sharing или любой другой функции, с которой Вы сейчас работаете. В качестве бонуса за Ваше участие, каждое оригинальное и воспроизводимое сообщение об ошибке увеличит Ваши шансы выиграть один из наших призов в тотализаторе. Подробности можно найти в блоге о выпуске бета-версии.