Batalha de serializadores JSON C # para .NET Core 3

Olá. Antecipando o início do curso "Desenvolvedor C #", preparamos uma tradução interessante para você e também oferecemos a você que assista ao registro da lição gratuitamente: "Design Pattern State (State)"










O recém-lançado .NET Core 3 trouxe consigo uma série de inovações. Além do C # 8 e do suporte para WinForms e WPF, a versão mais recente adicionou um novo JSON (des) serializador - System.Text.Json e, como o nome sugere, todas as suas classes estão neste namespace.



Esta é uma grande inovação. A serialização JSON é um fator importante em aplicativos da web.A maior parte da API REST de hoje depende disso. Quando seu cliente javascript envia JSON no corpo de uma solicitação POST, o servidor usa a desserialização JSON para convertê-lo em um objeto C #. E quando o servidor retorna um objeto em resposta, ele serializa esse objeto para JSON para que seu cliente javascript possa entendê-lo. Essas são grandes operações executadas em cada solicitação com objetos. Seu desempenho pode afetar significativamente o desempenho dos aplicativos, o que irei demonstrar agora.



Se você tem experiência com .NET, deve ter ouvido falar do excelente serializador Json.NET , também conhecido como Newtonsoft.Json . Então, por que precisamos de um novo serializador se já temos o adorável Newtonsoft.Json ? Embora o Newtonsoft.Json seja sem dúvida excelente, existem algumas boas razões para substituí-lo:



  • A Microsoft fez questão de usar novos tipos, como Span<T>, para melhorar o desempenho. Modificar uma grande biblioteca como o Newtonsoft sem quebrar a funcionalidade é muito difícil.
  • , HTTP, UTF-8. String .NET — UTF-16. Newtonsoft UTF-8 UTF-16, . UTF-8.
  • Newtonsoft , .NET Framework ( BCL FCL), . ASP.NET Core Newtonsoft, .


Neste artigo, vamos executar alguns benchmarks para ver como o novo serializador é muito melhor em termos de desempenho. Além disso, também compararemos Newtonsoft.Json e System.Text.Json com outros serializadores conhecidos e veremos como eles se comportam em relação um ao outro.



Batalha de serializadores



Aqui está nossa régua:



  • Newtonsoft.Json (também conhecido como Json.NET ) é o serializador atualmente o padrão da indústria. Ele foi integrado ao ASP.NET, embora fosse de terceiros. Pacote NuGet nº 1 de todos os tempos. Uma biblioteca vencedora de vários prêmios (provavelmente não tenho certeza).
  • 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 ( ).



  • Utf8Jso n é outro autoproclamado serializador C # para JSON mais rápido. Funciona com alocação de memória zero e lê / grava diretamente no binário UTF8 para melhor desempenho.





Observe que existem serializadores não JSON que são mais rápidos. Em particular, protobuf-net é um serializador binário que deve ser mais rápido do que qualquer um dos serializadores comparados neste artigo (que não foi testado por benchmarks embora).



Estrutura de referência



Os serializadores não são fáceis de comparar. Precisamos comparar a serialização e a desserialização. Precisaremos comparar diferentes tipos de classes (pequenas e grandes), listas e dicionários. E precisaremos comparar diferentes alvos de serialização: strings, fluxos e matrizes de caracteres (matrizes UTF-8). Esta é uma matriz de teste bastante grande, mas tentarei mantê-la o mais organizada e concisa possível.



Estaremos testando 4 funcionalidades diferentes:



  • Serialização para string
  • Serialização para um fluxo
  • Desserializar da string
  • Solicitações por segundo no aplicativo ASP.NET Core 3


Para cada um, testaremos diferentes tipos de objetos (que você pode ver no GitHub ):



  • Classe pequena com apenas 3 propriedades de tipo primitivo.
  • Classe grande com cerca de 25 propriedades, DateTime e alguns enums
  • Lista de 1000 elementos (classe pequena)
  • Dicionário de 1000 elementos (classe pequena)


Nem todos são benchmarks necessários, mas, na minha opinião, são suficientes para se ter uma ideia geral.

Para todos os pontos de referência que eu usei BenchmarkDotNet no seguinte sistema: 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. Você pode encontrar o próprio projeto de benchmark no GitHub .


Todos os testes serão executados apenas em projetos .NET Core 3.



Referência 1: Serialização para String



A primeira coisa que verificaremos é serializar nossa amostra de objetos em uma string.



O código de benchmark em si é bastante simples (veja no 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);
    }   
}


A classe de teste acima é genérica, então podemos testar todos os nossos objetos com o mesmo código, por exemplo:



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


Depois de executar todas as classes de teste com todos os serializadores, obtivemos os seguintes resultados:





Indicadores mais precisos podem ser encontrados aqui


  • Utf8Json é o mais rápido até agora, mais de 4x mais rápido do que Newtonsoft.Json e System.Text.Json . Esta é uma diferença notável.
  • Jil também é muito rápido, cerca de 2,5x mais rápido que Newtonsoft.Json e System.Text.Json.
  • Na maioria dos casos, o novo serializador System.Text.Json tem um desempenho melhor do que o Newtonsoft.Json em cerca de 10%, exceto para o Dicionário, onde acabou sendo 10% mais lento.
  • O DataContractJsonSerializer mais antigo é muito pior do que todos os outros.
  • ServiceStack fica bem no meio, mostrando que não é mais o serializador de texto mais rápido. Pelo menos para JSON.


Referência 2: serialização para stream



O segundo conjunto de testes é praticamente o mesmo, exceto que serializamos em um fluxo. O código de referência está aqui . Resultados:







Números mais precisos podem ser encontrados aqui . Agradecimentos a Adam Sitnik e Ahson Khan por me ajudarem a fazer o System.Text.Json funcionar.


Os resultados são muito semelhantes aos do teste anterior. Utf8Json e Jil são 4 vezes mais rápidos do que outros. Jil é muito rápido, perdendo apenas para Utf8Json . O DataContractJsonSerializer ainda é o mais lento na maioria dos casos. A Newtonsoft funciona de forma muito semelhante ao System.Text.Json na maioria dos casos , exceto para dicionários em que a Newtonsoft tem uma vantagem notável .



Teste de referência 3: desserializando da corda



O próximo conjunto de testes trata da desserialização de uma string. O código de teste pode ser encontrado aqui .







Números mais precisos podem ser encontrados aqui .


Estou tendo alguma dificuldade para executar o DataContractJsonSerializer para este benchmark, portanto, ele não está incluído nos resultados. Caso contrário, vemos que Jil é o mais rápido na desserialização , Utf8Json está em segundo lugar. Eles são 2 a 3 vezes mais rápidos do que System.Text.Json . E o System.Text.Json é cerca de 30% mais rápido que o Json.NET .



Até agora, descobriu-se que o popular Newtonsoft.Json e o novo System.Text.Json apresentam desempenho significativamente pior do que seus concorrentes. Este foi um resultado bastante inesperado para mim devido à popularidade do Newtonsoft.Jsone todo o entusiasmo em torno do novo Microsoft System.Text.Json de alto desempenho . Vamos dar uma olhada em um aplicativo ASP.NET.



Comparativo 4: o número de solicitações por segundo no servidor .NET



Conforme mencionado anteriormente, a serialização JSON é muito importante porque está constantemente presente nas APIs REST. As solicitações HTTP para um servidor usando o tipo de conteúdo application/jsonprecisarão serializar ou desserializar o objeto JSON. Quando o servidor aceita a carga em uma solicitação POST, o servidor é desserializado do JSON. Quando o servidor retorna um objeto em sua resposta, ele serializa JSON. A comunicação cliente-servidor moderna depende muito da serialização JSON. Portanto, para testar o cenário "real", faz sentido criar um servidor de teste e medir seu desempenho.



Eu fui inspirado pelo teste de desempenho da Microsoftem que eles criaram um aplicativo de servidor MVC e verificaram as solicitações por segundo. Os benchmarks da Microsoft testam System.Text.Json e Newtonsoft.Json . Neste artigo faremos o mesmo, exceto que iremos compará-los com Utf8Json , que provou ser um dos serializadores mais rápidos em testes anteriores.

Infelizmente, não consegui integrar o ASP.NET Core 3 com Jil, então o benchmark não o inclui. Tenho certeza de que isso pode ser feito com mais esforço, mas, infelizmente.


A criação deste teste provou ser mais difícil do que antes. Eu primeiro criei um aplicativo ASP.NET Core 3.0 MVC, assim como no benchmark da Microsoft. Eu adicionei um controlador para testes de desempenho, semelhante ao do teste da Microsoft :



[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;
}


Quando o cliente chama o endpoint DeserializeThousandSmallClassList, o servidor aceita o texto JSON e desserializa o conteúdo. É assim que testamos a desserialização. Quando o cliente ligar SerializeThousandSmallClassList, o servidor retornará uma lista de 1000 SmallClassitens e, assim, serializará o conteúdo em JSON.



Em seguida, precisamos cancelar o registro de cada solicitação para que isso não afete o resultado:



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


Agora precisamos alternar entre System.Text.Json , Newtonsoft e Utf8Json . É fácil com os dois primeiros. Para System.Text.Json, você não precisa fazer nada. Para mudar para Newtonsoft.Json, basta adicionar uma linha a ConfigureServices :



public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews()
    //   Newtonsoft.   -     System.Text.Json
    .AddNewtonsoftJson()
    ;


Para Utf8Json, precisamos adicionar formatadores de mídia personalizados InputFormattere OutputFormatter. Não foi tão fácil, mas no final encontrei uma boa solução na internet e, depois de vasculhar as configurações, deu certo. Também existe um pacote NuGet com formatadores, mas não funciona com o 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);
        }
    }
}


Agora, para ASP.NET, use este formatador:



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());
    });
}


Então este é o servidor. Agora, sobre o cliente.



Cliente C # para medir solicitações por segundo



Também criei um aplicativo cliente C #, embora os clientes JavaScript prevaleçam na maioria dos cenários do mundo real. Não importa para nossos propósitos. Aqui está o código:



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();
    }
}


Este cliente enviará solicitações continuamente por 1 segundo, contando-as.



resultados



Então, sem mais delongas, aqui estão os resultados:







Indicadores mais precisos podem ser encontrados aqui


Utf8Json superou outros serializadores por uma margem enorme. Esta não foi uma grande surpresa após os testes anteriores.



Em termos de serialização, Utf8Json é 2x mais rápido que System.Text.Json e 4x mais rápido que Newtonsoft . Para desserialização, Utf8Json é 3,5 vezes mais rápido que System.Text.Json e 6 vezes mais rápido que Newtonsoft .



A única surpresa para mim aqui é como o Newtonsoft.Json funciona mal... Isso provavelmente se deve ao problema com o UTF-16 e o ​​UTF-8. O protocolo HTTP funciona com texto UTF-8. A Newtonsoft converte esse texto em tipos de string .NET, que são UTF-16. Essa sobrecarga não está presente em Utf8Json ou System.Text.Json , que funcionam diretamente com UTF-8.



É importante notar que essas referências não devem ser 100% confiáveis, pois podem não refletir totalmente o cenário real. E é por causa disso:



  • Executei tudo na minha máquina local - cliente e servidor. Em um cenário do mundo real, o servidor e o cliente estão em máquinas diferentes.
  • . , . . - . , , . , , GC. Utf8Json, .
  • Microsoft ( 100 000). , , , , .
  • . , - - .


Considerando todas as coisas, esses resultados são incríveis. Parece que o tempo de resposta pode ser significativamente melhorado escolhendo o serializador JSON certo. Mudar de Newtonsoft para System.Text.Json aumentará o número de solicitações em 2 a 7 vezes, e mudar de Newtonsoft para Utf8Json aumentará em 6 a 14 vezes. Isso não é inteiramente justo, porque um servidor real fará muito mais do que apenas aceitar argumentos e retornar objetos. Provavelmente fará outras coisas também, como trabalhar com bancos de dados e, portanto, executar alguma lógica de negócios, portanto, o tempo de serialização pode ser menos importante. No entanto, esses números são incríveis.



conclusões



Vamos resumir:



  • System.Text.Json , Newtonsoft.Json ( ). Microsoft .
  • , Newtonsoft.Json System.Text.Json. , Utf8Json Jil 2-4 , System.Text.Json.
  • , Utf8Json ASP.NET . , , , ASP.NET.


Isso significa que todos devemos mudar para Utf8Json ou Jil? A resposta para isso é ... talvez. Lembre -se de que o Newtonsoft.Json resistiu ao teste do tempo e se tornou o serializador mais popular por um motivo. Ele oferece suporte a muitos recursos, foi testado com todos os tipos de casos extremos e tem toneladas de soluções e alternativas documentadas. Ambos System.Text.Json e Newtonsoft.Json são muito bem suportados. A Microsoft continuará investindo recursos e esforços no System.Text.Json para que você possa contar com um ótimo suporte. Considerando que Jil e Utf8Jsonrecebeu muito poucos commits no ano passado. Na verdade, parece que eles não tiveram muita manutenção nos últimos 6 meses.



Uma opção é combinar vários serializadores em seu aplicativo. Atualize para serializadores mais rápidos para integração ASP.NET para desempenho superior, mas continue a usar Newtonsoft.Json em sua lógica de negócios para obter o máximo de seu conjunto de recursos.



Espero que tenha gostado deste artigo. Boa sorte)



Outros benchmarks



Vários outros benchmarks comparando serializadores diferentes



Quando a Microsoft anunciou System.Text.Json, eles mostraram seu próprio benchmark comparando System.Text.Json e Newtonsoft.Json . Além de serialização e desserialização, este benchmark testa a classe Document para acesso aleatório, Reader e Writer. Eles também demonstraram seu teste Query Per Second , que me inspirou a criar o meu próprio.



O repositório .NET Core GitHub inclui um conjunto de benchmarks semelhantes aos descritos neste artigo. Observei atentamente seus testes para ter certeza de não cometer erros. Você pode encontrá-los emSolução de micro-benchmarks .



Jil tem seus próprios benchmarks que comparam Jil , Newtonsoft , Protobuf, e ServiceStack .



Utf8Json postou um conjunto de benchmarks disponíveis no GitHub . Eles também testam serializadores binários.



Alois Kraus fez excelentes testes detalhados dos serializadores .NET mais populares, incluindo serializadores JSON, serializadores binários e serializadores XML. Seu benchmark inclui benchmarks para .NET Core 3 e .NET Framework 4.8.






Saiba mais sobre o curso.






Consulte Mais informação:






All Articles