Мне всегда нравится понимать, как выглядит нижняя граница. Какова абсолютная самая высокая производительность, на которую мы можем рассчитывать? Я считаю это очень важным, поскольку это задает базовый уровень для всего остального.
Необходимым предупреждением здесь является риск экстраполяции слишком многого из такого тривиального образца. Мы должны принимать эти данные такими, какие они есть: базовыми. Они не являются репрезентативными для реальной бизнес-логики. Простое добавление некоторых операций ввода-вывода значительно увеличит время обработки. Обычно ввод-вывод в 1 000 раз — 1 000 000 раз медленнее, чем код.
Лямбда-функция Minimal
Проект Minimal определяет лямбда-функцию, которая принимает поток и возвращает пустой ответ. Она не имеет бизнес-логики и включает только необходимые библиотеки. Также отсутствует десериализация полезной нагрузки. Это функция Lambda с наименьшим количеством накладных расходов.
using System.IO;
using System.Threading.Tasks;
namespace Benchmark.Minimal {
public sealed class Function {
//--- Methods ---
public async Task<Stream> ProcessAsync(Stream request)
=> Stream.Null;
}
}
Данные бенчмарка для .NET 6 на x86-64
Данные наглядно показывают, что фаза INIT примерно одинакова для всех конфигураций памяти ниже порога в 3 008 МБ. Как упоминалось в статье Анатомия жизненного цикла AWS Lambda, фаза INIT всегда выполняется на полной скорости.
Холодная фаза INVOKE примерно в 10 раз медленнее для 128 МБ, чем для 1 024 МБ. Однако сумма всех теплых фаз INVOKE всего в ~3 раза медленнее. Тем не менее, стоимость улучшенной производительности выше менее чем на 5%.
Удивительно, что даже на таком тривиальном примере мы уже можем оценить тонкий баланс между производительностью и стоимостью.
Размер памяти | Начальная | Используемая холодная | Всего холодный старт | Всего использовано теплой (100) | Стоимость (мк$) |
---|---|---|---|---|---|
128 МБ | 235.615 | 620.921 | 856.536 | 365.519 | 22.25509 |
256 МБ | 238.296 | 315.731 | 554.027 | 150.124 | 22.14107 |
512MB | 241.193 | 136.89 | 378.083 | 124.686 | 22.37980 |
1024MB | 239.972 | 60.804 | 300.776 | 115.53 | 23.13891 |
1769MB | 241.005 | 37.623 | 278.628 | 116.322 | 24.63246 |
5120MB | 218.112 | 37.009 | 255.121 | 119.559 | 33.24730 |
Полноразмерное изображение
Полноразмерное изображение
Минимальная продолжительность холодного запуска для .NET 6
Неудивительно, что наименьшая продолжительность холодного запуска была достигнута при использовании конфигурации с наибольшим объемом памяти. Многоуровневая компиляция также помогла снизить этот показатель. Однако ReadyToRun не оказал значительного влияния, что вполне ожидаемо, поскольку наш минимальный проект почти не содержит кода.
Более примечательно, что архитектура ARM64 оказалась медленнее при сопоставимых конфигурациях памяти, чем архитектура x86-64.
Архитектура | Размер памяти | Многоуровневый | Ready2Run | PreJIT | Init | Использованный холодный старт | Всего Холодный старт |
---|---|---|---|---|---|---|---|
arm64 | 5120MB | да | нет | нет | 211.006 | 30.165 | 241.171 |
x86_64 | 1024 МБ | да | нет | нет | 213.085 | 33.173 | 246.258 |
x86_64 | 1769МБ | да | нет | нет | 215.754 | 24.164 | 239.918 |
x86_64 | 5120MB | да | нет | нет | 198.771 | 24.094 | 222.865 |
Полноразмерное изображение
Минимальная стоимость выполнения для .NET 6
Еще одним неудивительным результатом является то, что архитектура ARM64 обеспечивает самую низкую стоимость выполнения, поскольку ее цена за единицу продукции на 20% ниже. Аналогично, конфигурация памяти находится в нижней части — всего 256 МБ.
Более интересным является то, что многоуровневая компиляция всегда обходится дороже. Это интуитивно понятно, поскольку требуется дополнительное время на повторную обработку кода. После этого можно поспорить между настройками ReadyToRun и PreJIT.
Архитектура | Размер памяти | Многоуровневая | Ready2Run | PreJIT | Init | Используемая холодная память | Всего использовано тепло (100) | Стоимость (мк$) |
---|---|---|---|---|---|---|---|---|
arm64 | 256 МБ | нет | нет | нет | 266.026 | 378.676 | 158.064 | 21.98914228 |
arm64 | 256 МБ | нет | нет | да | 288.025 | 371.274 | 161.529 | 21.97601788 |
arm64 | 256 МБ | нет | да | нет | 264.304 | 361.657 | 164.619 | 21.95426344 |
arm64 | 256 МБ | нет | да | да | 287.762 | 361.285 | 160.248 | 21.93844936 |
Полноразмерное изображение
Что насчет .NET Core 3.1?
Я сомневался, стоит ли упоминать об этом, поскольку .NET Core 3.1 выходит из эксплуатации в декабре 2022 года, но разница в производительности для базового варианта просто ошеломляющая.
Лямбда-функция, использующая .NET Core 3.1 с 512 МБ, на 40% быстрее при холодном запуске, чем функция, использующая .NET 6 с 5120 МБ!
Я просто потрясен таким результатом. Все, что я могу сделать, это напомнить себе, что этот базовый тест не является репрезентативным для реального кода.
Архитектура | Размер памяти | Многоуровневый | Ready2Run | PreJIT | Init | Использованный холодный старт | Всего Холодный старт |
---|---|---|---|---|---|---|---|
x86_64 | 512 МБ | да | нет | нет | 150.129 | 6.903 | 157.032 |
x86_64 | 1024 МБ | да | нет | нет | 148.376 | 6.081 | 154.457 |
x86_64 | 1769МБ | да | нет | нет | 148.338 | 5.972 | 154.31 |
Полноразмерное изображение
Аналогично, стоимость выполнения ниже в .NET Core 3.1, но не так значительно. Тем не менее, для .NET 6 было всего 4 конфигурации, которые достигли стоимости ниже 22 мк$. Для .NET Core 3.1 существует 39 конфигураций со стоимостью ниже 21 мк$!
Интересно, что 4 конфигурации с наименьшей стоимостью следуют аналогичной схеме: ARM64, 128 МБ, отсутствие многоуровневой компиляции, а также выбор ReadyToRun и PreJIT.
Архитектура | Размер памяти | Многоуровневая компиляция | Ready2Run | PreJIT | Init | Используемая холодная память | Всего использовано тепло (100) | Стоимость (мк$) |
---|---|---|---|---|---|---|---|---|
arm64 | 128 МБ | нет | нет | нет | 162.366 | 102.693 | 110.096 | 20.55465044 |
arm64 | 128 МБ | нет | нет | да | 186.627 | 98.641 | 112.327 | 20.55161642 |
arm64 | 128 МБ | нет | да | нет | 161.989 | 88.677 | 110.391 | 20.53178133 |
arm64 | 128 МБ | нет | да | да | 185.923 | 85.289 | 117.811 | 20.53850086 |
Полноразмерное изображение
Заключение
На основе контрольных примеров мы можем установить эти нижние границы.
Для .NET 6:
- Продолжительность холодного старта: 223 мс
- Стоимость выполнения: 21,94 мк$
Для .NET Core 3.1:
- Продолжительность холодного запуска: 154 мс
- Стоимость выполнения: 20,53µ$
Если ничего принципиально не изменится, не стоит ожидать, что мы сможем добиться большего, чем эти базовые значения.
Что дальше
В следующем посте я собираюсь провести сравнительный анализ сериализаторов JSON. В частности, популярную библиотеку Newtonsoft JSON.NET, встроенное пространство имен System.Text.Json и новые генераторы исходников JSON в .NET 6.