Как стать автором
Обновить
819.56
OTUS
Цифровые навыки от ведущих экспертов

Битва C# JSON сериализаторов для .NET Core 3

Время на прочтение 13 мин
Количество просмотров 34K
Автор оригинала: Michael Shpilt
Всем привет. В преддверии старта курса «Разработчик C#» подготовили для вас интересный перевод, а также предлагаем бесплатно посмотреть запись урока: «Шаблон проектирования Состояние (State)»





Недавно выпущенный .NET Core 3 принес с собой ряд нововведений. Помимо C# 8 и поддержки WinForms и WPF, в последнем релизе был добавлен новый JSON (де)сериализатор — System.Text.Json, и, как следует из его названия, все его классы находятся в этом пространстве имен.

Это серьезное нововведение. Сериализация JSON — важный фактор в веб-приложениях. На нее полагается большая часть сегодняшнего REST API. Когда ваш javascript клиент отправляет JSON в теле POST запроса, сервер использует десериализацию JSON для преобразования его в C# объект. И когда сервер возвращает в ответ объект, он сериализует этот объект в JSON, чтобы ваш javascript клиент мог его понять. Это большие операции, которые выполняются для каждого запроса с объектами. Их производительность может значительно повлиять на производительность приложений, что я и собираюсь сейчас продемонстрировать.

Если у вас есть опыт работы с .NET, то вы должны были слышать о превосходном сериализаторе Json.NET, также известном как Newtonsoft.Json. Так зачем же нам новый сериализатор, если у нас уже есть прекрасный Newtonsoft.Json? Хотя Newtonsoft.Json несомненно великолепен, есть несколько веских причин для его замены:

  • Microsoft стремилась использовать новые типы, таких как Span<T>, для повышения производительности. Изменить такую ​​огромную библиотеку, как Newtonsoft, без нарушения функциональности, очень сложно.
  • Большинство сетевых протоколов, включая HTTP, используют текст типа UTF-8. String в .NET — UTF-16. Newtonsoft в процессе работы перекодирует строки из UTF-8 в UTF-16, что снижает производительность. Новый сериализатор напрямую использует UTF-8.
  • Поскольку Newtonsoft является сторонней библиотекой, а не частью .NET Framework (классов BCL или FCL), у вас могут получаться проекты с зависимостями от разных версий. Сам ASP.NET Core зависит от Newtonsoft, что порой результирует во множестве конфликтов версий.

В этой статье мы собираемся провести несколько бенчмарков, чтобы увидеть, насколько новый сериализатор лучше в плане производительности. Кроме того, мы также сравним Newtonsoft.Json и System.Text.Json с другими известными сериализаторами и посмотрим, как они справляются по отношению друг к другу.

Битва сериализаторов


Вот наша линейка:

  • Newtonsoft.Json (также известный как Json.NET) — сериализатор, в настоящее время являющийся стандартом в отрасли. Был интегрирован в ASP.NET, хотя и являлся сторонним. Пакет NuGet №1 на все времена. Отмеченная множеством наград библиотека (наверное, точно не знаю).
  • System.Text.Json — новый сериализатор от Microsoft. Якобы быстрее и лучше Newtonsoft.Json. По умолчанию интегрирован с новыми проектами ASP.NET Core 3. Часть платформы .NET, поэтому никаких зависимостей NuGet не требуется (и больше никаких конфликтов версий).
  • DataContractJsonSerializer — старый сериализатор, разработанный Microsoft, который был интегрирован в предыдущие версии ASP.NET до тех пор, пока его не заменил Newtonsoft.Json.
  • Jil — быстрый JSON сериализатор на основе Sigil

  • ServiceStack — .NET сериализатор в JSON, JSV и CSV. Самопровозглашенный самый быстрый сериализатор текста .NET (то есть не двоичный).

  • Utf8Json — еще один самопровозглашенный самый быстрый сериализатор C# в JSON. Работает с нулевым выделением памяти и читает/записывает непосредственно в двоичный код UTF8 для повышения производительности.


Обратите внимание, что существуют не-JSON сериализаторы, которые работают быстрее. В частности, protobuf-net — это двоичный сериализатор, который должен быть быстрее, любого из сравниваемых сериализаторов в этой статье (что, тем не менее, не проверено бенчмарками).

Структура бенчмарка


Сериализаторы сравнивать не так-то просто. Нам нужно будет сравнить сериализацию и десериализацию. Нам нужно будет сравнить разные типы классов (маленькие и большие), списки и словари. И нам нужно будет сравнить разные цели сериализации: строки, потоки и массивы символов (массивы UTF-8). Это довольно большая матрица тестов, но я постараюсь сделать ее как можно более организованной и лаконичной.

Мы будем тестировать 4 разных функциональности:

  • Сериализация в строку
  • Сериализация в поток
  • Десериализации из строки
  • Запросы в секунду на приложении ASP.NET Core 3

Для каждой, мы будем тестировать различные типы объектов (которые вы можете увидеть в GitHub):

  • Небольшие класс всего с 3 свойствами примитивного типа.
  • Большой класс с примерно 25 свойствами, DateTime и парой перечислений
  • Список из 1000 элементов (небольшого класса)
  • Словарь из 1000 элементов (небольшого класса)

Это далеко не все необходимые бенчмарки, но, на мой взгляд, их достаточно для получения общего представления.
Для всех бенчмарков я использовал BenchmarkDotNet на следующей системе: BenchmarkDotNet=v0.11.5, OS=Windows 10.0.17134.1069 (1803/April2018Update/Redstone4) Intel Core i7-7700HQ CPU 2.80GHz (Kaby Lake), 1 CPU, 8 logical and 4 physical cores. .NET Core SDK=3.0.100. Host : .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), 64bit RyuJIT. Сам проект бенчмарка можете найти на GitHub.

Все тесты будут проходить только в проектах .NET Core 3.

Бенчмарк 1: Сериализация в строку


Первое, что мы проверим, — это сериализация нашей выборки объектов в строку.

Сам код бенчмарка довольно прост (см. на GitHub):

public class SerializeToString<T> where  T : new()
{
 
    private T _instance;
    private DataContractJsonSerializer _dataContractJsonSerializer;
 
    [GlobalSetup]
    public void Setup()
    {
        _instance = new T();
        _dataContractJsonSerializer = new DataContractJsonSerializer(typeof(T));
    }
 
    [Benchmark]
    public string RunSystemTextJson()
    {
        return JsonSerializer.Serialize(_instance);
    }
 
    [Benchmark]
    public string RunNewtonsoft()
    {
        return JsonConvert.SerializeObject(_instance);
    }
 
    [Benchmark]
    public string RunDataContractJsonSerializer()
    {
        using (MemoryStream stream1 = new MemoryStream())
        {
            _dataContractJsonSerializer.WriteObject(stream1, _instance);
            stream1.Position = 0;
            using var sr = new StreamReader(stream1);
            return sr.ReadToEnd();
        }
    }
 
    [Benchmark]
    public string RunJil()
    {
        return Jil.JSON.Serialize(_instance);
    }
 
    [Benchmark]
    public string RunUtf8Json()
    {
        return Utf8Json.JsonSerializer.ToJsonString(_instance);
    }
 
    [Benchmark]
    public string RunServiceStack()
    {
        return SST.JsonSerializer.SerializeToString(_instance);
    }   
}

Приведенный выше тестовый класс является обобщенным, поэтому мы можем тестировать все наши объекты с помощью одного и того же кода, например:

BenchmarkRunner.Run<SerializeToString<Models.BigClass>>();

После запуска всех тестовых классов со всеми сериализаторами, мы получили следующие результаты:


Более точные показатели можно посмотреть здесь

  • Utf8Json является самым быстрым на сегодняшний день, более чем в 4 раза быстрее, чем Newtonsoft.Json и System.Text.Json. Это поразительная разница.
  • Jil также очень быстр, примерно в 2,5 раза быстрее, чем Newtonsoft.Json и System.Text.Json.
  • В большинстве случаев новый сериализатор System.Text.Json работает лучше, чем Newtonsoft.Json, примерно на 10%, за исключением Dictionary, где он оказался на 10% медленнее.
  • Более старый DataContractJsonSerializer намного хуже всех остальных.
  • ServiceStack расположился прямо посередине, показывая, что это уже не самый быстрый сериализатор текста. По крайней мере, для JSON.

Бенчмарк 2: Сериализация в поток


Второй набор тестов почти такой же, за исключением того, что мы сериализуем в поток. Код бенчмарка здесь. Результаты:



Более точные показатели можно посмотреть здесь. Спасибо Адаму Ситнику и Ахсону Хану за то, что помогли мне заставить работать System.Text.Json.

Результаты очень похожи на предыдущий тест. Utf8Json и Jil в 4 раза быстрее остальных. Jil работает очень быстро, уступая лишь Utf8Json. DataContractJsonSerializer в большинстве случаев по-прежнему самый медленный. Newtonsoft в большинстве случаев работает почти также как System.Text.Json, за исключением словарей, где есть заметное преимущество Newtonsoft.

Бенчмарк 3: Десериализация из строки


Следующий набор тестов касается десериализации из строки. Код теста можно найти здесь.



Более точные показатели можно посмотреть здесь.

У меня возникли некоторые трудности с запуском DataContractJsonSerializer для этого бенчмарка, поэтому он не включен в результаты. А в остальном мы видим, что в десериализации Jil быстрее всех, Utf8Json — на втором месте. Они в 2-3 раза быстрее, чем System.Text.Json. А System.Text.Json примерно на 30% быстрее, чем Json.NET.

Пока получается, что популярный Newtonsoft.Json и новый System.Text.Json имеют значительно худшую производительность, чем их конкуренты. Это было для меня довольно неожиданным результатом из-за популярности Newtonsoft.Json и всей шумихи вокруг нового топ-перформера Microsoft System.Text.Json. Давайте проверим это в приложении ASP.NET.

Бенчмарк 4: Количество запросов в секунду на .NET сервере


Как упоминалось ранее, сериализация JSON очень важна, потому что она постоянно присутствует в REST API. HTTP-запросы к серверу, использующему тип содержимого application/json, должны будут сериализовать или десериализовать JSON объект. Когда сервер принимает полезную нагрузку в POST запросе, сервер десериализует из JSON. Когда сервер возвращает объект в своем ответе, он сериализует JSON. Современное клиент-серверное взаимодействие во многом зависит от сериализации JSON. Поэтому для тестирования «реального» сценария имеет смысл создать тестовый сервер и измерить его производительность.

Меня вдохновил тест производительности Microsoft, в котором они создали серверное приложение MVC и проверяли количество запросов в секунду. Бенчмарки Microsoft тестируют System.Text.Json и Newtonsoft.Json. В этой статье мы сделаем то же самое, за исключением того, что мы собираемся сравнить их с Utf8Json, который показал себя одним из самых быстрых сериализаторов в предыдущих тестах.
К сожалению, мне не удалось интегрировать ASP.NET Core 3 с Jil, поэтому бенчмарк не включает его. Я абсолютно уверен, что это возможно, если приложить больше усилий, но увы.

Создание этого теста оказалось более сложной задачей, чем раньше. Сначала я создал приложение MVC ASP.NET Core 3.0, как и в бенчмарке Майкрософт. Я добавил контроллер для тестов производительности, похожий на тот, что был в тесте Майкрософт:

[Route("mvc")]
public class JsonSerializeController : Controller
{
 
    private static Benchmarks.Serializers.Models.ThousandSmallClassList _thousandSmallClassList
        = new Benchmarks.Serializers.Models.ThousandSmallClassList();
    
    [HttpPost("DeserializeThousandSmallClassList")]
    [Consumes("application/json")]
    public ActionResult DeserializeThousandSmallClassList([FromBody]Benchmarks.Serializers.Models.ThousandSmallClassList obj) => Ok();
 
    [HttpGet("SerializeThousandSmallClassList")]
    [Produces("application/json")]
    public object SerializeThousandSmallClassList() => _thousandSmallClassList;
}

Когда клиент вызовет конечную точку DeserializeThousandSmallClassList, сервер примет текст JSON и десериализует содержимое. Так мы тестируем десериализацию. Когда клиент вызовет SerializeThousandSmallClassList, сервер вернет список из 1000 SmallClass элементов и тем самым сериализует контент в JSON.

Затем нам нужно отменить логирование для каждого запроса, чтобы это не повлияло на результат:

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureLogging(logging =>
        {
            logging.ClearProviders();
            //logging.AddConsole();
        })
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });

Теперь нам нужен способ переключения между System.Text.Json, Newtonsoft и Utf8Json. С первыми двумя это несложно. Для System.Text.Json вообще ничего не нужно делать. Чтобы переключиться на Newtonsoft.Json, просто добавьте одну строку в ConfigureServices :

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews()
    // Раскомментируйте для Newtonsoft. Когда закомментировано - используется значение по умолчанию System.Text.Json
    .AddNewtonsoftJson()
    ;

Для Utf8Json нам нужно добавить кастомные модули форматирования мультимедиа InputFormatter и OutputFormatter. Это было не так просто, но в конце концов я нашел хорошее решение в интернете, и после того, как я покопался в настройках, все заработало. Также есть NuGet пакет с модулями форматирования, но он не работает с ASP.NET Core 3.

internal sealed class Utf8JsonInputFormatter : IInputFormatter
{
    private readonly IJsonFormatterResolver _resolver;
 
    public Utf8JsonInputFormatter1() : this(null) { }
    public Utf8JsonInputFormatter1(IJsonFormatterResolver resolver)
    {
        _resolver = resolver ?? JsonSerializer.DefaultResolver;
    }
 
    public bool CanRead(InputFormatterContext context) => context.HttpContext.Request.ContentType.StartsWith("application/json");
 
    public async Task<InputFormatterResult> ReadAsync(InputFormatterContext context)
    {
        var request = context.HttpContext.Request;
 
        if (request.Body.CanSeek && request.Body.Length == 0)
            return await InputFormatterResult.NoValueAsync();
 
        var result = await JsonSerializer.NonGeneric.DeserializeAsync(context.ModelType, request.Body, _resolver);
        return await InputFormatterResult.SuccessAsync(result);
    }
}
 
internal sealed class Utf8JsonOutputFormatter : IOutputFormatter
{
    private readonly IJsonFormatterResolver _resolver;
 
    public Utf8JsonOutputFormatter1() : this(null) { }
    public Utf8JsonOutputFormatter1(IJsonFormatterResolver resolver)
    {
        _resolver = resolver ?? JsonSerializer.DefaultResolver;
    }
 
    public bool CanWriteResult(OutputFormatterCanWriteContext context) => true;
 
    
    public async Task WriteAsync(OutputFormatterWriteContext context)
    {
        if (!context.ContentTypeIsServerDefined)
            context.HttpContext.Response.ContentType = "application/json";
 
        if (context.ObjectType == typeof(object))
        {
            await JsonSerializer.NonGeneric.SerializeAsync(context.HttpContext.Response.Body, context.Object, _resolver);
        }
        else
        {
            await JsonSerializer.NonGeneric.SerializeAsync(context.ObjectType, context.HttpContext.Response.Body, context.Object, _resolver);
        }
    }
}

Теперь, чтобы ASP.NET использовал эти модули форматирования:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews()
 
    // Раскомментируйте для Newtonsoft
    //.AddNewtonsoftJson()
 
   // Раскомментируйте для Utf8Json
    .AddMvcOptions(option =>
    {
        option.OutputFormatters.Clear();
        option.OutputFormatters.Add(new Utf8JsonOutputFormatter1(StandardResolver.Default));
        option.InputFormatters.Clear();
        option.InputFormatters.Add(new Utf8JsonInputFormatter1());
    });
}

Итак, это сервер. Теперь о клиенте.

Клиент C# для измерения количества запросов в секунду


Я также создал клиентское приложение на C#, хотя в большинстве реальных сценариев будут превалировать JavaScript клиенты. Для наших целей это не имеет значения. Вот код:

public class RequestPerSecondClient
{
    private const string HttpsLocalhost = "https://localhost:5001/";
 
    public async Task Run(bool serialize, bool isUtf8Json)
    {
        await Task.Delay(TimeSpan.FromSeconds(5));
        
        var client = new HttpClient();
        var json = JsonConvert.SerializeObject(new Models.ThousandSmallClassList());
 
       // Для разогрева, просто на всякий случай
        for (int i = 0; i < 100; i++)
        {
            await DoRequest(json, client, serialize);
        }
 
        int count = 0;
 
        Stopwatch sw = new Stopwatch();
        sw.Start();
 
        while (sw.Elapsed < TimeSpan.FromSeconds(1))
        {
            count++;
            await DoRequest(json, client, serialize);
        }
        
        Console.WriteLine("Requests in one second: " + count);
    }
 
    
    private async Task DoRequest(string json, HttpClient client, bool serialize)
    {
        if (serialize)
            await DoSerializeRequest(client);
        else
            await DoDeserializeRequest(json, client);
    }
    
    private async Task DoDeserializeRequest(string json, HttpClient client)
    {
        var uri = new Uri(HttpsLocalhost + "mvc/DeserializeThousandSmallClassList");
        var content = new StringContent(json, Encoding.UTF8, "application/json");
        var result = await client.PostAsync(uri, content);
        result.Dispose();
    }
 
    private async Task DoSerializeRequest(HttpClient client)
    {
        var uri = HttpsLocalhost + "mvc/SerializeThousandSmallClassList";
        var result = await client.GetAsync(uri);
        result.Dispose();
    }
}

Этот клиент будет непрерывно отправлять запросы в течение 1 секунды, подсчитывая их.

Результаты


Итак, без лишних слов, вот результаты:



Более точные показатели можно посмотреть здесь

Utf8Json с огромным отрывом превзошел другие сериализаторы. Это не было большим сюрпризом после предыдущих тестов.

Что касается сериализации, Utf8Json в 2 раза быстрее, чем System.Text.Json, и в 4 раза быстрее, чем Newtonsoft. Для десериализации Utf8Json в 3,5 раза быстрее, чем System.Text.Json, и в 6 раз быстрее, чем Newtonsoft.

Единственный сюрприз для меня здесь — то, насколько плохо работает Newtonsoft.Json. Вероятно, это связано с проблемой UTF-16 и UTF-8. Протокол HTTP работает с текстом UTF-8. Newtonsoft преобразует этот текст в строковые типы .NET, которые являются UTF-16. Эти накладные расходы не присутствуют ни в Utf8Json, ни в System.Text.Json, которые работают напрямую с UTF-8.

Важно отметить, что этим бенчмаркам не следует доверять на 100%, поскольку они могут не полностью отражать реальный сценарий. И вот почему:

  • Я запускал все на своем локальном компьютере — как клиент, так и сервер. В реальном сценарии сервер и клиент находятся на разных машинах.
  • Клиент отправляет запросы один за другим в одном потоке. Это означает, что сервер не принимает более одного запроса за раз. В реальном сценарии ваш сервер будет принимать запросы в нескольких потоках с разных машин. Эти сериализаторы могут действовать по-разному при одновременном обслуживании нескольких запросов. Возможно, некоторые используют больше памяти для повышения производительности, что не так хорошо при одновременном выполнении нескольких операций. Или, возможно, некоторые результируют в давлении GC. Это маловероятно с Utf8Json, который не использует аллокаций.
  • В тесте Microsoft они получали гораздо больше запросов в секунду (в некоторых случаях более 100 000). Конечно, это, вероятно, связано с указанными выше двумя пунктами и меньшей полезной нагрузкой, но все же это подозрительно.
  • В контрольных показателях легко ошибиться. Возможно, я что-то упустил или что сервер можно оптимизировать с помощью какой-то конфигурации.

С учетом всего вышесказанного эти результаты довольно невероятны. Кажется, что можно значительно улучшить время отклика, правильно подобрав сериализатор JSON. Переход с Newtonsoft на System.Text.Json увеличит количество запросов в 2-7 раз, а переход с Newtonsoft на Utf8Json улучшит в 6–14 раз. Это не совсем справедливо, потому что настоящий сервер будет делать гораздо больше, чем просто принимать аргументы и возвращать объекты. Вероятно, он будет делать и другие вещи, например, работать с базами данных и, следовательно, исполнять некоторую бизнес-логику, поэтому время сериализации может играть меньшую роль. Тем не менее, эти цифры невероятны.

Выводы


Давайте подытожим:

  • Новый сериализатор System.Text.Json в большинстве случаев быстрее, чем Newtonsoft.Json (во всех бенчмарках). Мое уважение Microsoft за хорошо проделанную работу.
  • Сторонние сериализаторы оказались быстрее, чем Newtonsoft.Json и System.Text.Json. В частности, Utf8Json и Jil примерно в 2-4 раза быстрее, чем System.Text.Json.
  • Сценарий подсчета количества запросов в секунду показал, что Utf8Json можно интегрировать с ASP.NET и значительно увеличить пропускную способность запросов. Как уже упоминалось, это не доподлинный сценарий реальных условий, и я рекомендую провести дополнительные тесты, если вы планируете изменить сериализаторы в своем приложении ASP.NET.

Значит ли это, что мы все должны перейти на Utf8Json или Jil? Ответ на это… возможно. Помните, что Newtonsoft.Json не просто так выдержал испытание временем и стал самым популярным сериализатором. Он поддерживает множество функций, был протестирован со всеми типами пограничных случаев и содержит массу документированных решений и обходных путей. И System.Text.Json, и Newtonsoft.Json очень хорошо поддерживаются. Microsoft продолжит вкладывать ресурсы и усилия в System.Text.Json, поэтому вы можете рассчитывать на отличную поддержку. Тогда как Jil и Utf8Json получили очень мало коммитов за последний год. На самом деле, похоже, что за последние 6 месяцев у них толком и не было особого технического обслуживания.

Один из вариантов — объединить несколько сериализаторов в вашем приложении. Перейдите на более быстрые сериализаторы для интеграции с ASP.NET для достижения превосходной производительности, но продолжайте использовать Newtonsoft.Json в бизнес-логике, чтобы получать максимальную выгоду от его набора функций.

Надеюсь, вам понравилась эта статья. Удачи)

Другие бенчмарки


Несколько других бенчмарков сравнивающих различные сериализаторы

Когда Microsoft анонсировала System.Text.Json, они продемонстрировали собственный бенчмарк, сравнивающий System.Text.Json и Newtonsoft.Json. Помимо сериализации и десериализации, этот бенчмарк тестирует класс Document для произвольного доступа, Reader и Writer. Они также продемонстрировали свой тест «Количество запросов в секунду», который вдохновил меня на создание собственного.

Репозиторий .NET Core GitHub включает в себя набор бенчмарков, подобных тем, что описаны в этой статье. Я очень внимательно посмотрел на их тесты, чтобы убедиться, что сам не делаю ошибок. Вы можете найти их в Micro-benchmarks solution.

У Jil есть собственные бенчмарки, которые сравнивают Jil, Newtonsoft, Protobuf и ServiceStack.

Utf8Json опубликовал набор бенчмарков, доступных на GitHub. Они также тестируют двоичные сериализаторы.

Алоис Краус провел отличное всестороннее тестирование самых популярных сериализаторов .NET, включая сериализаторы JSON, двоичные сериализаторы и сериализаторы XML. Его бенчмарк включает тесты производительности .NET Core 3 и .NET Framework 4.8.



Узнать о курсе подробнее.



Читать ещё:


Теги:
Хабы:
+23
Комментарии 15
Комментарии Комментарии 15

Публикации

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия
Представитель
OTUS