Эта страница может содержать автоматически переведенный текст.

Производительность C# Native AOT

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

Сравнение производительности 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 позволяют сравнивать различные конфигурации с точки зрения производительности. Однако результаты зависят от операционной системы и архитектуры процессора. Вам необходимо запустить собственные тесты производительности в целевой среде, чтобы найти оптимальную конфигурацию развертывания.