Бенчмаркинг сериализаторов .NET JSON на AWS Lambda

Практически весь код .NET на AWS Lambda имеет дело с сериализацией JSON. Исторически сложилось так, что библиотека Newtonsoft Json.NET была наиболее подходящей. Совсем недавно в .NET Core 3 появилась System.Text.Json. Обе библиотеки используют рефлексию для построения логики сериализации. Новейшая техника, называемая генератором источников, была представлена в .NET 6 и использует подход, основанный на компиляции, который позволяет обойтись без отражения.

Итак, теперь у нас есть три подхода на выбор, в связи с чем возникает вопрос: Есть ли явный победитель или все гораздо сложнее?

Для этих эталонов код десериализует довольно раздутую структуру данных JSON, взятую из документации API GitHub, а затем возвращает пустой ответ.

Newtonsoft Json.NET

Эта библиотека существует так давно и настолько популярна, что сломала счетчик загрузок, когда он превысил 2 миллиарда на nuget.org. С тех пор счетчик был исправлен, но эта впечатляющая веха осталась!

using System.IO;
using System.Threading.Tasks;
using Amazon.Lambda.Core;
using Amazon.Lambda.Serialization.Json;

[assembly:LambdaSerializer(typeof(JsonSerializer))]

namespace Benchmark.NewtonsoftJson {

    public sealed class Function {

        //--- Methods ---
        public async Task<Stream> ProcessAsync(Root request) {
            return Stream.Null;
        }
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Минимальная продолжительность холодного запуска

Четыре самых быстрых холодных старта используют архитектуру x86-64 и ReadyToRun. Самый быстрый вариант также использует Tiered Compilation. Опция PreJIT всегда медленнее при включении, но все равно попадает в четверку лучших.

Архитектура Размер памяти Tiered Ready2Run PreJIT Init Использованный холодный старт Всего Холодный старт
x86_64 1769МБ нет да нет 262.942 186.097 449.039
x86_64 1769МБ нет да да 317.328 151.456 468.784
x86_64 1769МБ да да нет 236.714 170.028 406.742
x86_64 1769МБ да да да 295.209 137.727 432.936

Полноразмерное изображение

Минимальная стоимость исполнения

Признаюсь, здесь я был немного удивлен. Я ожидал, что ARM64 будет очевидным выбором, поскольку стоимость выполнения на 20% ниже. Однако этого не произошло. Вместо этого мы получили разделение 50/50, причем x86-64 выигрывает совсем немного.

Также интересно, что при самой низкой стоимости выполнения всегда используется опция PreJIT. Это интуитивно понятно, поскольку эта опция переносит часть затрат с первой фазы INVOKE на свободную фазу INIT, а в остальном имеет лишь небольшой штраф за накладные расходы.

Аналогично, Tiered Compilation отключена для всех, поскольку она вносит дополнительные накладные расходы во время теплых фаз INVOKE.

Наиболее интересным для меня является то, что ARM64 дешевле с 512 МБ памяти, в то время как x86-64 дешевле с 256 МБ. Возможно, это просто странность, но она подчеркивает, что ничто не очевидно, и почему бенчмаркинг реального кода так важен!

Архитектура Размер памяти Многоуровневый Ready2Run PreJIT Init Используемая холодная память Всего использовано тепло (100) Стоимость (мк$)
arm64 256 МБ нет да да 346.884 1598.711 406.117 26.88279408
arm64 512MB нет да да 348.615 753.974 238.541 26.81680042
x86_64 256 МБ нет да да 317.574 1186.12 377.718 26.71600553
x86_64 512 МБ нет да да 314.298 562.768 234.544 26.84427746

Полноразмерное изображение

System.Text.Json — Отражение

System.Text.Json был представлен в .NET Core 3. Первоначальный выпуск не был достаточно функциональным, чтобы стать убедительным выбором. Однако теперь это уже не так. В .NET 5 все мои опасения были устранены, и с тех пор я предпочитаю именно этот вариант. К сожалению, нам пришлось подождать до .NET 6, который является LTS, чтобы он стал поддерживаться на AWS Lambda.

using System.IO;
using System.Threading.Tasks;
using Amazon.Lambda.Core;
using Amazon.Lambda.Serialization.SystemTextJson;

[assembly:LambdaSerializer(typeof(DefaultLambdaJsonSerializer))]

namespace Benchmark.SystemTextJson {

    public sealed class Function {

        //--- Methods ---
        public async Task<Stream> ProcessAsync(Root request) {
            return Stream.Null;
        }
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Минимальная продолжительность холодного запуска

Как и в случае с Json.NET, 4 самых быстрых по длительности холодного старта используют архитектуру x86-64. В отличие от предыдущего бенчмарка, во всех них включена многоуровневая компиляция. ReadyToRun дает небольшое преимущество, но незначительное. Вероятно, это связано с тем, что код сериализации JSON находится в .NET framework. Как и раньше, PreJIT замедляет работу, но все равно это одна из 4 самых быстрых конфигураций.

Архитектура Размер памяти Многоуровневый Ready2Run PreJIT Init Использованный холодный старт Всего Холодный старт
x86_64 1769МБ да нет нет 231.55 97.37 328.92
x86_64 1769 МБ да нет да 276.791 74.063 350.854
x86_64 1769МБ да да нет 226.864 93.64 320.504
x86_64 1769МБ да да да 273.615 71.244 344.859

Полноразмерное изображение

Минимальная стоимость выполнения

Как и в бенчмарке Json.NET, в 4-х самых дешевых по стоимости выполнения отключена многоуровневая компиляция и включена опция PreJIT. Также результаты равномерно распределяются между ARM64 и x86-64.

И снова оптимальная конфигурация использует архитектуру x86-64 с включенным ReadyToRun. Однако на этот раз все 4 оптимальные конфигурации сходятся на 256 МБ памяти.

Архитектура Размер памяти Многоуровневая Ready2Run PreJIT Init Используемая холодная память Всего использовано тепло (100) Стоимость (мк$)
arm64 256 МБ нет нет да 335.019 977.84 344.601 24.60815771
arm64 256 МБ нет да да 330.424 966.123 347.232 24.57787356
x86_64 256 МБ нет нет да 302.287 688.363 341.735 24.49208483
x86_64 256 МБ нет да да 293.871 679.57 299.889 24.28108858

Полноразмерное изображение

System.Text.Json — генератор исходного кода

Новое в .NET 6 — возможность генерировать код сериализации JSON во время компиляции вместо того, чтобы полагаться на отражение во время выполнения.

Лично я, как человек, который много заботится о производительности, считаю генераторы исходников действительно интересным дополнением к инструментарию разработчика. Однако я не считаю эту итерацию готовой к производству, поскольку в ней отсутствуют некоторые функции, на которые я полагаюсь. В частности, отсутствие пользовательских конвертеров типов для переопределения стандартного поведения сериализации JSON является для меня препятствием. Тем не менее, для некоторых небольших проектов он может быть жизнеспособным. Моя главная рекомендация здесь — тщательно проверять выходные данные, чтобы гарантировать, что любые изменения поведения будут уловлены во время разработки.

using System.Text.Json.Serialization;
using Amazon.Lambda.Core;
using Amazon.Lambda.Serialization.SystemTextJson;
using Benchmark.SourceGeneratorJson;

[assembly: LambdaSerializer(typeof(SourceGeneratorLambdaJsonSerializer<FunctionSerializerContext>))]

namespace Benchmark.SourceGeneratorJson;

[JsonSerializable(typeof(Root))]
public partial class FunctionSerializerContext : JsonSerializerContext { }

public sealed class Function {

    //--- Methods ---
    public async Task<Stream> ProcessAsync(Root request) {
        return Stream.Null;
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Минимальная продолжительность холодного запуска

На этот раз 4 самых быстрых холодных запуска использовали Tiered Compilation и ReadyToRun. Поскольку генераторы исходников создают больше кода для jit, логично, что эти опции улучшают производительность холодного запуска, поскольку это их цель. Также, в отличие от предыдущих бенчмарков, ARM64 и x86-64 теперь конкурируют за первое место. PreJIT снова немного замедляет работу, но все же попадает в четверку лучших.

Несмотря на то, что ARM64 наконец-то появился в бенчмарке Minimum Cold Start Duration, архитектура x86-64 по-прежнему занимает два первых места.

Архитектура Размер памяти Многоуровневый Ready2Run PreJIT Init Использованный холодный старт Всего Холодный старт
arm64 1769МБ да да нет 249.244 65.429 314.673
arm64 1769MB да да да 276.097 60.221 336.318
x86_64 1769МБ да да нет 240.88 53.104 293.984
x86_64 1769МБ да да да 265.776 46.327 312.103

Полноразмерное изображение

Минимальная стоимость выполнения

Результаты этого бенчмарка немного сложнее для анализа. Впервые у нас нет симметрии между вариантами. Вместо этого, ARM64 занимает 3 из 4 самых дешевых мест. То же самое справедливо для опции PreJIT и конфигурации с 256 МБ памяти.

Как и в бенчмарке Json.NET, самые дешевые конфигурации используют ReadyToRun, и, как и во всех бенчмарках стоимости выполнения, Tiered Compilation отключена.

Архитектура Размер памяти Tiered Ready2Run PreJIT Init Используемая холодная память Всего использовано тепло (100) Стоимость (мк$)
arm64 256 МБ нет да нет 287.093 702.015 294.423 23.52147561
arm64 256 МБ нет да да 311.507 660.822 295.178 23.38668193
arm64 512MB нет да да 312.017 315.322 204.109 23.66288998
x86_64 256 МБ нет да да 294.279 519.965 298.581 23.61061349

Полноразмерное изображение

Резюме

Вот наши наблюдаемые нижние границы для библиотек сериализации JSON, а также базовая производительность на .NET 6 для сравнения. Я опустил .NET Core 3.1, поскольку больше не считаю его подходящей целевой средой выполнения. Однако вы можете изучить набор результатов в интерактивной электронной таблице Google.

  • Базовый уровень для .NET 6
    • Продолжительность холодного старта: 223 мс
    • Стоимость выполнения: 21,94 мк$
  • Newtonsoft Json.NET
    • Продолжительность холодного запуска: 433 мс
    • Стоимость выполнения: 26.72 µ$
  • System.Text.Json — Reflection
    • Продолжительность холодного запуска: 321 мс
    • Стоимость выполнения: 24,28 µ$
  • System.Text.Json — Генератор источников
    • Продолжительность холодного запуска: 294 мс
    • Стоимость выполнения: 23,39 µ$

Не стоит удивляться, что Json.NET, который существует уже долгое время, накопил много мусора. Json.NET — это поистине швейцарский армейский нож для сериализации, и за эту гибкость приходится платить. Он добавляет не менее 210 мс к длительности холодного запуска, а также является самой дорогой библиотекой JSON для запуска.

Более новая библиотека System.Text.Json имеет неоспоримое преимущество в производительности и стоимости по сравнению с Json.NET. Она добавляет всего 100 мс к длительности холодного старта и на 9% дешевле по сравнению с Json.NET.

Однако явным победителем является новый генератор исходных текстов JSON с накладными расходами на холодный запуск всего 70 мс по сравнению с нашей базовой версией. Стоимость также на 12% ниже, чем у Json.NET. Тем не менее, недостаток функций может сделать его пока не самым лучшим выбором.

Когда речь идет о минимизации длительности холодного запуска, чем больше памяти, тем лучше. В этих тестах использовалось 1,769 МБ, что позволяет использовать большую часть доступной производительности vCPU, но не всю. Полная производительность vCPU достигается при 3 008 МБ, что почти удваивает затраты на 10% улучшение (источник).

Для минимизации затрат 256 МБ кажется предпочтительным выбором. Tiered Compilation никогда не следует использовать, но ReadyToRun полезен. Странным в этой конфигурации является то, что ReadyToRun производит код Tier0 (т.е. грязный JIT без инлайнинга, подъема и прочих вкусных вещей для производительности). С отключенной Tiered Compilation наш код никогда не будет оптимизирован дальше, насколько я знаю.

Что дальше

В следующем посте я собираюсь исследовать накладные расходы, вносимые AWS SDK. Поскольку большинство функций Lambda будут использовать его, я подумал, что будет полезно понять, каковы затраты на инициализацию.

Оцените статью
devanswers.ru
Добавить комментарий