Боремся с глобальным потеплением или AOT-компиляция в C#

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

Ahead-of-time компиляция была частью .NET ещё со времён выпуска первой версии .NET Framework. В .NET Framework была технология NGEN, которая предварительно генерировала нативный код и структуры данных во время установки программы .NET в глобальный кэш сборок. NGEN кэшировал код и структуры данных, необходимые среде выполнения для запуска установленной программы. Кэш был неполным - среда выполнения возвращалась к JIT-компиляции и загружалась по мере необходимости, но такой способ подходил для AOT-компиляции типичных приложений.

Среда выполнения Mono продолжила подход с кэшированием и дала возможность запуска без какой-либо генерации кода «на лету». Mono достигла этого благодаря вложениям в предварительно генерируемый код для универсальных шаблонов и различных заглушек, которые не были учтены в NGEN.

Несмотря на то, что такую компиляцию можно назвать досрочной, она отличается от аналогов в C, Go, Swift или в Rust. Рализация AOT в распространённых рантаймах .NET обладает рядом преимуществ, о которых далее пойдёт речь.

JIT vs AOT

Распространённое заблуждение в различии ahead-of-time и just-in-time рантаймов состоит в том, что во внимание берётся только время генерации нативного кода. JIT-компилирующая среда выполнения сгенерирует нативный код по запросу, когда приложение развёрнуто и запущено в целевом окружении. AOT-компилирующий рантайм формирует нативный код предварительно, как часть сборки приложения.

Источник заблуждения кроется в старом способе реализации AOT-компиляции в мейнстримных рантаймах. Они добавляли нативный код в сборку .NET считая это хорошей практикой. AOT-компилятор способен на большее.

Не все AOT одинаковы

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

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

Этим же вопросом задалась команда .NET около 11 лет назад. Тогда было ясно, что оптимизация существующей общеязыковой среды выполнения (CLR) для AOT будет недопустимо затратна. Так родилась новая среда выполнения, оптимизированная для AOT.

.NET рантайм для AOT

Анонсированный .NET Native дал прирост в 60% ко времени запуска по сравнению с NGEN. Эти улучшения стали возможными благодаря оптимизации среды выполнения и форматов файлов для AOT-компиляции.

Вот как выглядят программы в CoreRT после AOT-компиляции:

Заметьте, что такие вещи, как списки методов по типу и имена типов больше не существуют в данном формате. Они не нужны при AOT-компиляции программы. Реальные (не абстрактные) процессоры обрабатывают только код - им не нужно знать к какому типу принадлежит метод. Им также не нужна информация о количестве полей метода - они обращаются ко блоку памяти в определённом регистре.

Минимальные структуры данных в схеме - такие, как EEtype, описывающая тип System.String, содержат минимальное количество данных, необходимых для запуска программы .NET. Поле RelatedType в EEtype даёт возможность транслировать экземпляр System.Stringв System.Object. Слоты виртуальных таблиц поддерживают вызов виртуальных методов. BaseSizeподдерживает распределение объекта и сборку мусора.

Декомпиляция программы в подобном представлении имеет сложность аналогичную декомпиляции в C++.

Структуры данных, которыми оперирует эта минимальная среда выполнения .NET по факту похожи на структуры данных, которыми бы оперировала библиотека выполнения среды C++ - то же самое касается и размера среды выполнения. В минимальной конфигурации CoreRT может компилировать автономный исполняемый файл размером до ~400 kB, который включает полную среду выполнения и сборщик мусора (данные размера рассчитаны для x64 - целевые файлы x32 могут быть ещё меньше). В этой конфигурации используется сборщик мусора, который справляется с гигабайтами рабочей нагрузки в Azure.

Время запуска

Главное преимущество AOT-компиляции выражается во времени запуска. JIT-компиляция (или AOT рантайм, построенный на формате промежуточного языка) потратит значительное количество времени на поддержку запуска программы, но не на запуск кода. Процесс запуска будет выглядеть примерно так:

60% улучшение времени запуска, которое наблюдалось в Универсальной платформе Windows с .NET Native, также переводится на другие типы рабочей нагрузки. Вот как выглядит время запуска для ASP.NET с эталонным тестом, который использует команда производительности CoreCLR:

Время запуска не играет большой роли для «долгих» процессов, типа веб-серверов. Оно играет роль когда конечный пользователь сталкивается с задержками: время запуска - это тот момент времени, когда пользователь смотрит на песочные часы у курсора и на приложение на рабочем столе, на заставку в мобильном приложении или на пустую страницу в браузере.

Время до первой пользовательской инструкции

Интересная метрика: сколько времени проходит с момента создания процесса до выполнения первой строки вашего Main (). Прежде, чем запустить выполнение кода, среде выполнения предстоит проделать много другой работы. Реализовать метрику довольно просто. Для этого первой строчкой в Main() напишите вызов к API times для Linux или GetProcessTimes для Windows.

Эти API предоставят вам информацию о количестве времени, потраченного на работу фреймворка прежде, чем он приступит к выполнению первой строки вашей программы. Для таких языков, как C этим значением обычно будет 0 - первая строка вашей программы запустится прежде, чем у ОС появится возможность обновить статистику. Для приложений командной строки время запуска обычно равняется нулю.

Вот, как выглядит время до первой инструкции в разных .NET рантаймах:

Значение CoreRT - 0. Приложение запускается также быстро, как в C.

Размеры компиляции на выходе

Большое отличие между рантаймами JIT и AOT заключается в размерах развёрнутой автономной среды. CoreRT выигрывает тем, что рантайм написан на C#, в отличии от других .NET рантаймов, реализованных на C и C++. Управляемый код может быт отвязан, если не используется приложением. Для традиционных рантаймов часть среды выполнения обходится в фиксированный размер, который нельзя адаптировать для каждого приложения. AOT-среды могут быть гораздо меньше:

Что насчёт рефлексии?

В то время, как ЦП не нуждается в названиях ваших методов, а AOT-компилятор избавлен от необходимости транслировать эту информацию, API рефлексия позволяет найти любой тип, метод или поле по имени, и даёт доступ к дополнительной информации по этим сущностям, такой как подпись или имена параметров метода.

Компилятор CoreRT решает эту проблему попутной сортировкой информации рефлексии - она не обязательна для запуска программы и транслировать её не обязательно. Можно назвать дополнительные данные «сбором рефлексии». AOT-компилятор освобождает вас от затрат, если вы не используете данную функцию.

Без данных рефлексия становится ограниченной: можно использовать typeof, вызывать Object.GetType(), исследовать базовые типы и реализованные интерфейсы, но список методов, полей или названий типов становится невидимым.

Затраты на рефлексию остаются неисследованными в .NET: поскольку ни CoreRT, ни Mono не могут оперировать без метаданных промежуточного языка, отказ от метаданных невозможен для мейнстримных рантаймов. Однако, решение этого вопроса - прямой путь к развёрнутым средам размером менее мегабайта, что важно для таких задач, как WebAssembly.

Традиционно языки с AOT-компиляцией не имеют неограниченной рефлексии, как у .NET, и доказывают, что для завершения работы необязательно предоставлять всё рефлексии. Вещи, для которых сегодня используют рефлексию, могут быть сделаны без неё - во время сборки или компиляции. В этом отношении сборка обладает рядом преимуществ и для динамических рантаймов потому, что рефлексия медленна.

Что насчёт динамического кода?

.NET обеспечивает некоторые удобства для генерации нового кода в рантайме. Будь то Reflection.Emit, Assembly.LoadFrom, или даже что-то настолько простое, как MakeGenericType и MakeGenericMethod. Эти формы представляют проблему для среды выполнения AOT, так как эти вещи нельзя сделать досрочно по определению, или не для всех программ.

Вот где AOT-рантайм вынужден совершать загрузку «на лету». Важно, что в среде выполнения AOT затраты на динамические функции происходят только при явном использовании: не вызываются динамические API, нет избыточного времени исполнения.

Так, что такое CoreRT?

CoreRT - экспериментальная кроссплатформенная open-source среда выполнения .NET, заточенная на AOT-компиляции. Несмотря на свой экспериментальный статус, много частей CoreRT уже используются в поддерживаемых продуктах. CoreRT использует части .NET Native и складывает их воедино. Грубая процентная оценка долей, которыми CoreRT делится с CoreCLR и .NET Native:

Благодаря заимствованиям кода CoreRT улучшается каждый раз, когда улучшается CoreLib в CoreCLR или оптимизируется RyuJIT.

JIT и AOT - как бензиновый и электрический двигатели

Характеристики производительности AOT- и JIT-компиляций можно сравнить с характеристиками электрического и бензинового двигателей в машинах.

  • Электрические двигатели производят движение, не тепло. JIT-скомпилированное приложение .NET затратит значительное количество ресурсов для поддержки вещей, необходимых для запуска кода, но не для самого запуска.
  • На низкой скорости электродвигатели обеспечивают больший крутящий момент, чем бензиновые, что даёт лучшее ускорение. В AOT-компилированном приложении пик производительности доступен сразу. Ваше приложение работает на полной скорости с самого начала. Однако, в конце бензиновый двигатель превзойдёт электрический также, как время исполнения JIT-компиляции опередит AOT.
  • Электрические двигатели проще. При замене стека технологий появляется много сложностей с JIT-компиляцией. Эти сложности затрагивают разработчиков среды выполнения и её пользователей. Направление запуска нативного кода определяется динамической настройкой, которую производил рантайм на основании предыдущих характеристик программы.

Бензиновый и электрический двигатели имеют своё собственное место. Всегда приятно иметь возможность выбора, и хорошо что в .NET есть такая возможность

Источник

Погружаетесь в программирование на C# и .NET? Другие материалы по теме:

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

matyushkin
18 марта 2020

ТОП-10 книг по C#: от новичка до профессионала

Отобрали актуальные книги по C#, .NET, Unity c лучшими оценками. Расположил...
Библиотека программиста
25 августа 2019

Почему C# программисты скоро будут нарасхват

C# программисты становятся более востребованными благодаря развивающейся эк...
Библиотека программиста
12 марта 2018

Видеокурс по C# с нуля: от основ до полноценного приложения

Подробный видеокурс для изучающих C# с нуля. Пройдем путь от основ до напис...