Эта страница может содержать автоматически переведенный текст.
Производительность 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. Они используют следующую конфигурацию:
Сценарий | Аргументы сборки dotnet publish |
---|---|
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, которое является еще одной формой предварительной компиляции в .NET. Иногда это хорошая альтернатива Native AOT.
Вот текущие результаты сравнения производительности для .NET 9 Release Candidate (сентябрь 2024 г.):
Время запуска
Приложения AOT запускаются намного быстрее, чем управляемые версии. Это справедливо как для приложений Stage1, так и для Stage2 и для всех сред. Примеры результатов:
Сценарий | Время запуска (мс) |
---|---|
Stage2AotSpeedOpt | 100 |
Stage2Aot | 109 |
Stage2 | 528 |
Рабочий набор
Максимальный рабочий набор для приложений Native AOT меньше, чем для управляемых версий. В Linux управляемые версии используют примерно в 1,5–2 раза больше оперативной памяти, чем версии AOT. Например:
Сценарий | Максимальный рабочий набор (Мб) |
---|---|
Stage1Aot | 56 |
Stage1AotSpeedOpt | 57 |
Stage1 | 126 |
В Windows разница меньше. Особенно для Stage2:
Сценарий | Максимальный рабочий набор (Мб) |
---|---|
Stage2Aot | 152 |
Stage2AotSpeedOpt | 150 |
Stage2 | 167 |
Количество запросов в секунду
Большие значения RPS означают более быстрое приложение. Легкое приложение Stage1 обычно обрабатывает около 800-900 тыс. запросов в секунду. Более крупное приложение Stage2 обрабатывает только около 200 тыс. запросов.
Для приложения Stage2 версия .NET обрабатывает больше запросов, чем версии AOT во всех средах. Скорость версии Stage2AotSpeedOpt иногда близка. Но обычно она находится между Stage2 и Stage2Aot. Вот типичные результаты:
Сценарий | RPS |
---|---|
Stage2 | 235 008 |
Stage2AotSpeedOpt | 215 637 |
Stage2Aot | 194 264 |
Результаты для приложения Stage1 схожи на Intel Linux и Intel Windows. Однако на Ampere Linux AOT превосходит управляемую версию. Примеры результатов из Ampere Linux:
Сценарий | RPS |
---|---|
Stage1AotSpeedOpt | 929 524 |
Stage1Aot | 912 344 |
Stage1 | 844 659 |
Итак, среда и код приложения могут существенно влиять на скорость. Имеет смысл запустить собственные тесты производительности, чтобы оценить преимущества Native AOT для вашего проекта. Давайте напишем собственные бенчмарки без тестовой инфраструктуры Microsoft.
Тестирование производительности приложений Native AOT
Мы будем использовать 2 типа бенчмарков. Первый основан на BenchmarkDotNet — популярной библиотеке для бенчмаркинга кода .NET. Эти бенчмарки сравнивают чистую скорость, исключая время запуска.
Второй основан на инструменте hyperfine. Он позволяет сравнивать время выполнения двух команд оболочки. Эти тесты сравнивают общую скорость, включая время запуска.
Мы не будем сравнивать потребление памяти здесь. На данный момент диагност NativeMemoryProfiler
в
BenchmarkDotNet не поддерживает среду выполнения 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 to PNG получите бесплатный лицензионный ключ с ограниченным сроком действия на
странице Скачать PDF библиотеку C# .NET. Вам необходимо применить лицензионный ключ в Helper.cs
.
BenchmarkDotNet
Эти бенчмарки находятся в проекте NativeAotBenchmarks
. Мы сравниваем результаты для
RuntimeMoniker.NativeAot80 и RuntimeMoniker.Net80. По умолчанию BenchmarkDotNet собирает код Native
AOT с настройкой OptimizationPreference=Speed
.
BenchmarkDotNet выполняет 6 или более итераций разогрева. Это помогает JIT предварительно скомпилировать код и собрать некоторую статистику. Таким образом, такие бенчмарки исключают время запуска из сравнения.
Сжатие строк
Тест CompressString
для сжатия строк использует длинную строку с повторяющимися символами.
Распространенной ошибкой будет генерация случайной строки. В таком случае тесты для Native AOT и
.NET 8 будут использовать разные входные строки. Можно использовать и случайные строки, но вам нужно
инициализировать генератор случайных чисел с тем же начальным числом.
Версия 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 to 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 точек на дюйм. Скорость почти одинаковая:
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 шанс собрать больше статистики времени выполнения и сгенерировать более быстрый код.
Распространенная ситуация следующая. В первый раз вы запускаете бенчмарки с одной итерацией. Версия 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 имеет доступ к информации о времени выполнения. В долго работающих приложениях он может регенерировать более эффективный код на основе динамической оптимизации с использованием профиля и других методов.
Бенчмарки ASP.NET позволяют сравнивать различные конфигурации с точки зрения производительности. Однако результаты зависят от операционной системы и архитектуры процессора. Вам необходимо запустить собственные тесты производительности в целевой среде, чтобы найти оптимальную конфигурацию развертывания.