Эта страница может содержать автоматически переведенный текст.
Производительность C# Native AOT
Насколько быстро работают приложения .NET Native AOT по сравнению с обычным управляемым кодом? Может ли AOT обогнать JIT? Как бенчмаркать приложения Native AOT?

Эта статья входит в серию о Native AOT в .NET. Если вы не знакомы с Native AOT, сначала прочитайте часть Как разрабатывать приложения Native AOT в .NET.
В этой статье сравниваются производительность .NET и Native AOT. Сначала мы рассмотрим официальные бенчмарки Microsoft. Они позволяют сравнивать разные варианты развертывания .NET для простых приложений ASP.NET.
Затем вы узнаете, как запускать собственные бенчмарки с помощью инструментов BenchmarkDotNet и hyperfine. Такие бенчмарки позволяют измерять скорость кода в вашей среде.
Бенчмарки ASP.NET
Команда ASP.NET поддерживает надежную инфраструктуру для тестирования производительности. Они тестируют различные сценарии в разных средах.
Нас больше всего интересуют бенчмарки Native AOT. Основной источник данных — следующая панель PowerBI. Данные там основаны на 3 «китах»: тестовых приложениях, сценариях развертывания и метриках.
Тестовые приложения
Исходный код бенчмарков и тестовых приложений можно найти в репозитории aspnet/Benchmarks.
Бенчмарки Native AOT сравнивают 3 типа приложений:
- Stage1 - минимальный API на основе HTTP и JSON. Исходный код приложения находится в
/src/BenchmarksApps/BasicMinimalApi. - Stage1Grpc - похожий API на основе gRPC (
/src/BenchmarksApps/Grpc/BasicGrpc) - Stage2 - полнофункциональное веб-приложение с базой данных и аутентификацией (
/src/BenchmarksApps/TodosApi)
Сценарии развертывания .NET
Тестовые приложения запускаются в разных средах. На данный момент бенчмарки используют виртуальные машины Windows и Linux с 28 ядрами. Также есть отдельные Linux-среды для процессоров ARM и Intel.
Приложения также тестируются в разных конфигурациях. Приложение в определенной конфигурации задает «сценарий».
Вы можете удерживать клавишу Ctrl (или ⌘), чтобы выбрать несколько сценариев или сред на панели PowerBI.
Метрики
Бенчмарки собирают базовые метрики для каждого развернутого приложения. Например, тесты измеряют число запросов в секунду (RPS), время запуска, максимальный рабочий набор памяти.
Это позволяет сравнивать значения метрик для разных конфигураций одного и того же приложения.
Сравнение производительности
Мы сравним сценарии StageX с StageXAot и StageXAotSpeedOpt. Они используют следующую конфигурацию:
| Scenario | dotnet publish build arguments |
|---|---|
| StageX | PublishAot=false EnableRequestDelegateGenerator=false |
| Stage2Aot | PublishAot=true StripSymbols=true |
| Stage2AotSpeedOpt | PublishAot=true StripSymbols=true OptimizationPreference=Speed |
Все сценарии выше также используют переменную среды DOTNET_GCDynamicAdaptationMode=1.
Сценарии StageXAotSpeedOpt позволяют оценить влияние настройки OptimizationPreference = Speed.
Вы также можете посмотреть сценарии StageXTrimR2RSingleFile. Такие сценарии соответствуют усеченному развертыванию ReadyToRun, которое является еще одной формой компиляции ahead-of-time в .NET. Иногда это хорошая альтернатива Native AOT.
Вот текущие результаты сравнения производительности для .NET 9 Release Candidate (сентябрь 2024):
Время запуска
Приложения AOT запускаются намного быстрее, чем управляемые версии. Это верно и для приложений Stage1 и Stage2, и для всех сред. Примеры результатов:
| Scenario | Startup time (ms) |
|---|---|
| Stage2AotSpeedOpt | 100 |
| Stage2Aot | 109 |
| Stage2 | 528 |
Рабочий набор
Максимальный рабочий набор для приложений Native AOT меньше, чем для управляемых версий. В Linux управляемые версии используют примерно в 1,5–2 раза больше ОЗУ, чем версии AOT. Например:
| Scenario | Max working set (MB) |
|---|---|
| Stage1Aot | 56 |
| Stage1AotSpeedOpt | 57 |
| Stage1 | 126 |
В Windows разница меньше. Особенно для Stage2:
| Scenario | Max working set (MB) |
|---|---|
| Stage2Aot | 152 |
| Stage2AotSpeedOpt | 150 |
| Stage2 | 167 |
Запросов в секунду
Больше значения RPS означают более быстрое приложение. Легкое приложение Stage1 обычно обрабатывает около 800-900K запросов в секунду. Более крупное приложение Stage2 обрабатывает только около 200K запросов.
Для приложения Stage2 версия .NET обрабатывает больше запросов, чем версии AOT, во всех средах. Скорость версии Stage2AotSpeedOpt иногда близка. Но обычно она находится между Stage2 и Stage2Aot. Вот типичные результаты:
| Scenario | RPS |
|---|---|
| Stage2 | 235,008 |
| Stage2AotSpeedOpt | 215,637 |
| Stage2Aot | 194,264 |
Результаты для приложения Stage1 похожи на Intel Linux и Intel Windows. Однако на Ampere Linux AOT обгоняет управляемую версию. Пример результатов для Ampere Linux:
| Scenario | RPS |
|---|---|
| Stage1AotSpeedOpt | 929,524 |
| Stage1Aot | 912,344 |
| Stage1 | 844,659 |
Таким образом, среда и код приложения могут существенно влиять на скорость. Имеет смысл запускать собственные бенчмарки, чтобы оценить преимущества Native AOT для вашего проекта. Давайте напишем собственные бенчмарки без инфраструктуры тестирования Microsoft.
Бенчмаркинг приложений Native AOT
Мы будем использовать 2 типа бенчмарков. Первый основан на BenchmarkDotNet — популярной библиотеке для бенчмаркинга кода .NET. Эти бенчмарки сравнивают чистую скорость, исключая время запуска.
Второй основан на инструменте hyperfine. Он позволяет сравнивать время выполнения двух команд shell. Эти бенчмарки сравнивают общую скорость, включая время запуска.
Потребление памяти здесь сравниваться не будет. На данный момент диагностер NativeMemoryProfiler в BenchmarkDotNet не поддерживает runtime Native AOT. hyperfine тоже пока не отслеживает использование памяти.
Вы можете скачать исходный код из репозитория NativeAotBenchmarks на GitHub. Рекомендуем попробовать его в своей среде. В этой статье приведены результаты с ноутбука на Windows 11 с процессором Intel Core i9-13900H и 16 ГБ ОЗУ.
Убедитесь, что вы запускаете бенчмарки правильно. Вот общие рекомендации:
- Используйте сборку Release.
- Закройте все приложения, кроме процесса бенчмарка. Например, отключите антивирус, закройте Visual Studio и веб-браузер.
- Держите ноутбук подключенным к сети и используйте режим максимальной производительности.
- Используйте одни и те же входные данные в сравниваемых сценариях.
Тестовые случаи
Мы будем бенчмаркать 2 сценария в .NET 8:
1. Простой код C# для сжатия строки с использованием подсчета повторяющихся символов. Например, строка "aabcccccaaa" станет "a2b1c5a3":
string Compress(string s)
{
StringBuilder compressed = new(s.Length);
for (int i = 0; i < s.Length; ++i)
{
char c = s[i];
for (int j = i + 1; j <= s.Length; ++j)
{
if (j == s.Length || s[j] != c)
{
compressed.Append(c + $"{j - i}");
i = j - 1;
if (compressed.Length > s.Length)
return s;
break;
}
}
}
if (compressed.Length <= s.Length)
return compressed.ToString();
return s;
}
2. Более тяжелая задача конвертации PDF в PNG, использующая Docotic.Pdf.
Предварительные условия
Установите необходимые компоненты для развертывания .NET Native AOT.
Установите hyperfine, чтобы запускать соответствующие бенчмарки.
Для бенчмарков PDF в PNG получите бесплатный временно ограниченный лицензионный ключ на странице Скачать C# .NET библиотеку PDF. Вам нужно применить лицензионный ключ в Helper.cs.
BenchmarkDotNet
Эти бенчмарки находятся в проекте NativeAotBenchmarks. Мы сравниваем результаты для RuntimeMoniker.NativeAOT80 и RuntimeMoniker.Net80. По умолчанию BenchmarkDotNet собирает код Native AOT с настройкой OptimizationPreference=Speed.
BenchmarkDotNet выполняет 6 или более итераций прогрева. Это помогает JIT предварительно скомпилировать код и собрать некоторую статистику. Поэтому такие benhmarks исключают время запуска из сравнения.
Сжатие строки
Бенчмарк CompressString для сжатия строки использует длинную строку с повторяющимися символами. Распространенная ошибка — генерировать случайную строку. В таком случае бенчмарки для Native AOT и .NET 8 использовали бы разные входные строки. Можно использовать и случайные строки, но тогда нужно инициализировать генератор случайных чисел одним и тем же seed.
Версия Native AOT работает примерно в 1,08 раза быстрее, чем версия .NET 8:
| Method | Runtime | Mean | Error | StdDev |
|---|---|---|---|---|
| Compress | .NET 8.0 | 4.117 ms | 0.0553 ms | 0.0517 ms |
| Compress | NativeAOT 8.0 | 3.809 ms | 0.0403 ms | 0.0377 ms |
PDF в PNG
Бенчмарки PDF в PNG обрабатывают PDF-документы в памяти. Это позволяет исключить взаимодействие с файловой системой. Операции ввода-вывода с диском могут исказить результаты бенчмарка.
Мы тестируем скорость на двух PDF-документах. Первый, Banner Edulink One.pdf, более сложный. Он конвертируется в PNG с разрешением 72 dpi и требует больше времени на обработку. Для этого документа версия .NET 8 немного быстрее:
| Method | Runtime | Mean | Error | StdDev |
|---|---|---|---|---|
| Convert | .NET 8.0 | 1.103 s | 0.0156 s | 0.0146 s |
| Convert | NativeAOT 8.0 | 1.167 s | 0.0160 s | 0.0149 s |
Второй документ меньше и проще. Он конвертируется в PNG с разрешением 300 dpi. И скорость почти одинакова:
| Method | Runtime | Mean | Error | StdDev |
|---|---|---|---|---|
| Convert | .NET 8.0 | 290.1 ms | 5.78 ms | 6.88 ms |
| Convert | NativeAOT 8.0 | 288.3 ms | 4.44 ms | 3.94 ms |
hyperfine
Эти бенчмарки находятся в проекте NativeAotTestApp. Проект не использует настройку OptimizationPreference=Speed. Вы можете включить ее в NativeAotTestApp.csproj: <OptimizationPreference>Speed</OptimizationPreference>
Используйте скрипт benchmark.bat, чтобы запускать тесты в Windows. Вы можете преобразовать его в Bash для Unix/Linux-систем. Скрипт собирает версии приложения .NET 8 и Native AOT. Затем он сравнивает их производительность с помощью похожих команд: hyperfine --warmup 3 "net8-app.exe" "native-aot-app.exe"
Прогрев в hyperfine помогает запускать тестовые приложения на «теплых» кешах диска. В отличие от BenchmarkDotNet, прогрев hyperfine не помогает JIT. Поэтому бенчмарки hyperfine сравнивают общую скорость приложения, включая время запуска.
Наше тестовое приложение поддерживает аргумент количества итераций. Это позволяет повторять один и тот же код несколько раз в простом цикле:
for (int i = 0; i < iterationCount; ++i)
CompressString(args);
Идея состоит в том, чтобы уменьшить влияние разницы во времени запуска. Повторение одного и того же кода дает JIT возможность собрать больше runtime-статистики и сгенерировать более быстрый код.
Типичная ситуация такая. При первом запуске вы выполняете бенчмарки с одной итерацией. Версия Native AOT работает намного быстрее. Затем вы запускаете те же бенчмарки с несколькими итерациями, и общая скорость обеих версий становится одинаковой. Это означает, что после запуска управляемая версия на самом деле быстрее.
Сжатие строки
Для 100 000 итераций одного и того же сжатия входной строки производительность Native AOT лучше:
Benchmark 1: .NET 8 version (100000 iterations)
Time (mean ± σ): 151.5 ms ± 2.6 ms [User: 32.1 ms, System: 1.6 ms]
Range (min … max): 148.0 ms … 157.5 ms 19 runs
Benchmark 2: Native AOT version (100000 iterations)
Time (mean ± σ): 55.1 ms ± 3.1 ms [User: 15.0 ms, System: 2.1 ms]
Range (min … max): 51.6 ms … 65.9 ms 51 runs
Summary
Native AOT version ran 2.75 ± 0.16 times faster than .NET 8 version
Но при 10 000 000 итераций скорость становится почти одинаковой:
Benchmark 1: .NET 8 version (10000000 iterations)
Time (mean ± σ): 3.984 s ± 0.139 s [User: 2.946 s, System: 0.009 s]
Range (min … max): 3.790 s … 4.182 s 10 runs
Benchmark 2: Native AOT version (10000000 iterations)
Time (mean ± σ): 3.956 s ± 0.041 s [User: 2.848 s, System: 0.004 s]
Range (min … max): 3.888 s … 4.016 s 10 runs
Summary
Native AOT version ran 1.01 ± 0.04 times faster than .NET 8 version
PDF в PNG
Для одной итерации конвертации Banner Edulink One.pdf в PNG версия AOT работает примерно в 1,88 раза быстрее версии .NET 8:
Benchmark 1: .NET 8 version (1 iteration)
Time (mean ± σ): 2.417 s ± 0.104 s [User: 1.334 s, System: 0.116 s]
Range (min … max): 2.295 s … 2.629 s 10 runs
Benchmark 2: Native AOT version (1 iteration)
Time (mean ± σ): 1.288 s ± 0.011 s [User: 0.573 s, System: 0.123 s]
Range (min … max): 1.274 s … 1.310 s 10 runs
Для 20 итераций разница в скорости несущественна:
Benchmark 1: .NET 8 version (20 iterations)
Time (mean ± σ): 25.048 s ± 0.223 s [User: 13.278 s, System: 2.312 s]
Range (min … max): 24.751 s … 25.423 s 10 runs
Benchmark 2: Native AOT version (20 iterations)
Time (mean ± σ): 25.213 s ± 0.114 s [User: 12.661 s, System: 2.275 s]
Range (min … max): 25.042 s … 25.350 s 10 runs
Summary
.NET 8 version ran 1.01 ± 0.01 times faster than Native AOT version
Для 3BigPreview.pdf версия Native AOT быстрее даже при 100 итерациях:
Benchmark 1: .NET 8 version (100 iterations)
Time (mean ± σ): 10.009 s ± 0.152 s [User: 5.298 s, System: 0.567 s]
Range (min … max): 9.677 s … 10.189 s 10 runs
Benchmark 2: Native AOT version (100 iterations)
Time (mean ± σ): 8.336 s ± 0.070 s [User: 3.405 s, System: 0.505 s]
Range (min … max): 8.247 s … 8.459 s 10 runs
Summary
Native AOT version ran 1.20 ± 0.02 times faster than .NET 8 version
Заключение
Приложения Native AOT запускаются быстрее по сравнению с обычным .NET. Официальные бенчмарки также показывают, что приложения AOT имеют меньший объем потребления памяти.
Но после запуска управляемые приложения обычно показывают лучшую скорость. Это происходит потому, что JIT имеет доступ к runtime-информации. В долго работающих приложениях он может заново генерировать более эффективный код на основе динамической оптимизации с учетом профиля и других техник.
Бенчмарки ASP.NET позволяют сравнивать разные конфигурации с точки зрения производительности. Однако результаты зависят от операционной системы и архитектуры процессора. Вам нужно запускать собственные бенчмарки в целевой среде, чтобы найти оптимальную конфигурацию развертывания.