Batalla de serializadores JSON de C # para .NET Core 3

Hola. Anticipándonos al inicio del curso "C # Developer", hemos preparado una traducción interesante para usted y también le ofrecemos que vea el registro de la lección de forma gratuita: "Estado del patrón de diseño (estado)"










El .NET Core 3 recientemente lanzado trajo consigo una serie de innovaciones. Además de C # 8 y el soporte para WinForms y WPF, la última versión ha agregado un nuevo (de) serializador JSON: System.Text.Json , y como su nombre indica, todas sus clases están en este espacio de nombres.



Esta es una gran innovación. La serialización JSON es un factor importante en las aplicaciones web.La mayor parte de la API REST actual se basa en ella. Cuando su cliente javascript envía JSON en el cuerpo de una solicitud POST, el servidor usa la deserialización JSON para convertirlo en un objeto C #. Y cuando el servidor devuelve un objeto en respuesta, serializa este objeto en JSON para que su cliente javascript pueda entenderlo. Estas son operaciones grandes que se realizan en cada solicitud con objetos. Su rendimiento puede afectar significativamente el rendimiento de las aplicaciones, lo que voy a demostrar ahora.



Si tiene experiencia con .NET, entonces debe haber oído hablar del excelente serializador Json.NET , también conocido como Newtonsoft.Json . Entonces, ¿por qué necesitamos un nuevo serializador cuando ya tenemos el encantador Newtonsoft.Json ? Si bien Newtonsoft.Json es indudablemente excelente, hay algunas buenas razones para reemplazarlo:



  • Microsoft deseaba utilizar nuevos tipos, como Span<T>para mejorar el rendimiento. Modificar una biblioteca enorme como Newtonsoft sin romper la funcionalidad es muy 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, .


En este artículo, vamos a ejecutar algunos puntos de referencia para ver cuánto mejor es el nuevo serializador en términos de rendimiento. Además, también compararemos Newtonsoft.Json y System.Text.Json con otros serializadores conocidos y veremos cómo se manejan entre sí.



Batalla de serializadores



Aquí está nuestra regla:



  • Newtonsoft.Json (también conocido como Json.NET ) es el serializador que actualmente es el estándar de la industria. Estaba integrado en ASP.NET, aunque era un tercero. Paquete NuGet n. ° 1 de todos los tiempos. Una biblioteca ganadora de múltiples premios (probablemente no estoy seguro).
  • 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 es otro serializador de C # a JSON autoproclamado más rápido. Funciona con cero asignación de memoria y lee / escribe directamente en binario UTF8 para un mejor rendimiento.





Tenga en cuenta que existen serializadores que no son JSON y que son más rápidos. En particular, protobuf-net es un serializador binario que debería ser más rápido que cualquiera de los serializadores comparados en este artículo (que no ha sido probado por puntos de referencia).



Estructura de referencia



Los serializadores no son fáciles de comparar. Tendremos que comparar la serialización y la deserialización. Necesitaremos comparar diferentes tipos de clases (pequeñas y grandes), listas y diccionarios. Y necesitaremos comparar diferentes objetivos de serialización: cadenas, flujos y matrices de caracteres (matrices UTF-8). Esta es una matriz de prueba bastante grande, pero intentaré mantenerla lo más organizada y concisa posible.



Probaremos 4 funcionalidades diferentes:



  • Serialización a cadena
  • Serialización en una secuencia
  • Deserializar de cadena
  • Solicitudes por segundo en la aplicación ASP.NET Core 3


Para cada uno, probaremos diferentes tipos de objetos (que puede ver en GitHub ):



  • Clase pequeña con solo 3 propiedades de tipo primitivo.
  • Clase grande con aproximadamente 25 propiedades, DateTime y un par de enumeraciones
  • Lista de 1000 elementos (clase pequeña)
  • Diccionario de 1000 elementos (clase reducida)


No todos son puntos de referencia necesarios, pero, en mi opinión, son suficientes para tener una idea general.

Para todos los puntos de referencia Solía BenchmarkDotNet en el siguiente 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. Puede encontrar el proyecto de referencia en sí mismo en GitHub .


Todas las pruebas solo se ejecutarán en proyectos .NET Core 3.



Benchmark 1: serialización a cadena



Lo primero que comprobaremos es serializar nuestra muestra de objetos en una cadena.



El código de referencia en sí es bastante simple (ver en 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);
    }   
}


La clase de prueba anterior es genérica, por lo que podemos probar todos nuestros objetos con el mismo código, por ejemplo:



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


Después de ejecutar todas las clases de prueba con todos los serializadores, obtuvimos los siguientes resultados:





Se pueden encontrar indicadores más precisos aquí


  • Utf8Json es el más rápido hasta la fecha, más de 4 veces más rápido que Newtonsoft.Json y System.Text.Json . Ésta es una diferencia sorprendente.
  • Jil también es muy rápido, aproximadamente 2,5 veces más rápido que Newtonsoft.Json y System.Text.Json.
  • En la mayoría de los casos, el nuevo serializador System.Text.Json funciona mejor que Newtonsoft.Json en aproximadamente un 10%, excepto para Dictionary, donde terminó siendo un 10% más lento.
  • El DataContractJsonSerializer más antiguo es mucho peor que todos los demás.
  • ServiceStack se encuentra justo en el medio, lo que demuestra que ya no es el serializador de texto más rápido. Al menos para JSON.


Benchmark 2: serialización para transmitir



El segundo conjunto de pruebas es prácticamente el mismo, excepto que estamos serializando en una secuencia. El código de referencia está aquí . Resultados:







Se pueden encontrar cifras más precisas aquí . Gracias a Adam Sitnik y Ahson Khan por ayudarme a que System.Text.Json funcione.


Los resultados son muy similares a la prueba anterior. Utf8Json y Jil son 4 veces más rápidos que otros. Jil es muy rápido, solo superado por Utf8Json . DataContractJsonSerializer sigue siendo el más lento en la mayoría de los casos. Newtonsoft funciona de manera muy similar a System.Text.Json en la mayoría de los casos , excepto en los diccionarios donde Newtonsoft tiene una ventaja notable .



Punto de referencia 3: deserialización de una cadena



El siguiente conjunto de pruebas se ocupa de la deserialización de una cadena. El código de prueba se puede encontrar aquí .







Se pueden encontrar cifras más precisas aquí .


Tengo algunas dificultades para ejecutar DataContractJsonSerializer para este punto de referencia, por lo que no está incluido en los resultados. De lo contrario, vemos que Jil es el más rápido en deserialización , Utf8Json está en segundo lugar. Son 2-3 veces más rápidos que System.Text.Json . Y System.Text.Json es aproximadamente un 30% más rápido que Json.NET .



Hasta ahora, resulta que el popular Newtonsoft.Json y el nuevo System.Text.Json tienen un rendimiento significativamente peor que sus competidores. Este fue un resultado bastante inesperado para mí debido a la popularidad de Newtonsoft.Jsony toda la publicidad en torno al nuevo Microsoft System.Text.Json de alto rendimiento . Veámoslo en una aplicación ASP.NET.



Benchmark 4: la cantidad de solicitudes por segundo en el servidor .NET



Como se mencionó anteriormente, la serialización JSON es muy importante porque está constantemente presente en las API REST. Las solicitudes HTTP a un servidor que utilizan el tipo de contenido application/jsondeberán serializar o deserializar el objeto JSON. Cuando el servidor acepta la carga útil en una solicitud POST, el servidor deserializa desde JSON. Cuando el servidor devuelve un objeto en su respuesta, serializa JSON. La comunicación cliente-servidor moderna depende en gran medida de la serialización JSON. Por lo tanto, para probar el escenario "real", tiene sentido crear un servidor de prueba y medir su rendimiento.



Me inspiré en la prueba de rendimiento de Microsoft.en el que crearon una aplicación de servidor MVC y verificaron las solicitudes por segundo. Los puntos de referencia de Microsoft prueban System.Text.Json y Newtonsoft.Json . En este artículo haremos lo mismo, excepto que los vamos a comparar con Utf8Json , que ha demostrado ser uno de los serializadores más rápidos en pruebas anteriores.

Desafortunadamente, no pude integrar ASP.NET Core 3 con Jil, por lo que el punto de referencia no lo incluye. Estoy bastante seguro de que se puede hacer con más esfuerzo, pero lamentablemente.


La creación de esta prueba resultó ser más difícil que antes. Primero creé una aplicación ASP.NET Core 3.0 MVC, como en el benchmark de Microsoft. Agregué un controlador para las pruebas de rendimiento, similar al de la prueba de 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;
}


Cuando el cliente llama al punto final DeserializeThousandSmallClassList, el servidor aceptará el texto JSON y deserializará el contenido. Así es como probamos la deserialización. Cuando el cliente llama SerializeThousandSmallClassList, el servidor devolverá una lista de 1000 SmallClasselementos y, por lo tanto, serializará el contenido en JSON.



Luego, debemos cancelar el registro para cada solicitud para que no afecte el resultado:



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


Ahora necesitamos una forma de cambiar entre System.Text.Json , Newtonsoft y Utf8Json . Es fácil con los dos primeros. Para System.Text.Json no necesita hacer nada en absoluto. Para cambiar a Newtonsoft.Json, simplemente agregue una línea a ConfigureServices :



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


Para Utf8Json, necesitamos agregar formateadores de medios personalizados InputFormattery OutputFormatter. No fue tan fácil, pero finalmente encontré una buena solución en Internet , y después de investigar en la configuración, funcionó. También hay un paquete NuGet con formateadores, pero no funciona con 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);
        }
    }
}


Ahora para que ASP.NET use estos formateadores:



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


Entonces este es el servidor. Ahora sobre el cliente.



Cliente C # para medir solicitudes por segundo



También he creado una aplicación cliente de C #, aunque los clientes de JavaScript prevalecerán en la mayoría de los escenarios del mundo real. No importa para nuestros propósitos. Aquí está el 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á solicitudes continuamente durante 1 segundo, contándolas.



resultados



Entonces, sin más preámbulos, aquí están los resultados:







Se pueden encontrar indicadores más precisos aquí


Utf8Json superó a otros serializadores por un gran margen. Esto no fue una gran sorpresa después de las pruebas anteriores.



En términos de serialización, Utf8Json es 2 veces más rápido que System.Text.Json y 4 veces más rápido que Newtonsoft . Para la deserialización, Utf8Json es 3,5 veces más rápido que System.Text.Json y 6 veces más rápido que Newtonsoft .



La única sorpresa para mí aquí es lo mal que funciona Newtonsoft.Json... Esto probablemente se deba al problema de UTF-16 y UTF-8. El protocolo HTTP funciona con texto UTF-8. Newtonsoft convierte este texto a tipos de cadena .NET, que son UTF-16. Esta sobrecarga no está presente ni en Utf8Json ni en System.Text.Json , que funcionan directamente con UTF-8.



Es importante tener en cuenta que estos puntos de referencia no deben ser 100% confiables, ya que pueden no reflejar completamente el escenario real. Y es por eso:



  • Ejecuté todo en mi máquina local, tanto en el cliente como en el servidor. En un escenario del mundo real, el servidor y el cliente están en máquinas diferentes.
  • . , . . - . , , . , , GC. Utf8Json, .
  • Microsoft ( 100 000). , , , , .
  • . , - - .


A fin de cuentas, estos resultados son bastante increíbles. Parece que el tiempo de respuesta se puede mejorar significativamente eligiendo el serializador JSON correcto. Pasar de Newtonsoft a System.Text.Json aumentará el número de solicitudes de 2 a 7 veces, y cambiar de Newtonsoft a Utf8Json mejorará de 6 a 14 veces. Esto no es del todo justo, porque un servidor real hará mucho más que aceptar argumentos y devolver objetos. Probablemente también hará otras cosas, como trabajar con bases de datos y, por lo tanto, ejecutar alguna lógica empresarial, por lo que el tiempo de serialización puede ser menos importante. Sin embargo, estos números son increíbles.



conclusiones



Resumamos:



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


¿Significa esto que todos deberíamos cambiar a Utf8Json o Jil? La respuesta a eso es ... tal vez. Recuerde, Newtonsoft.Json ha resistido la prueba del tiempo y se ha convertido en el serializador más popular por una razón. Es compatible con muchas funciones, se ha probado con todo tipo de casos extremos y tiene toneladas de soluciones y soluciones documentadas. Tanto System.Text.Json como Newtonsoft.Json son muy compatibles. Microsoft seguirá invirtiendo recursos y esfuerzos en System.Text.Json para que pueda contar con un excelente soporte. Considerando que Jil y Utf8Jsonrecibió muy pocos compromisos en el último año. De hecho, parece que no han tenido mucho mantenimiento en los últimos 6 meses.



Una opción es combinar varios serializadores en su aplicación. Actualice a serializadores más rápidos para la integración ASP.NET para un rendimiento superior, pero continúe utilizando Newtonsoft.Json en su lógica empresarial para aprovechar al máximo su conjunto de características.



Espero que hayas disfrutado de este artículo. Buena suerte)



Otros puntos de referencia



Varios otros puntos de referencia comparando varios serializadores



Cuando Microsoft anunció System.Text.Json, mostró su propio punto de referencia comparando System.Text.Json y Newtonsoft.Json . Además de la serialización y deserialización, este punto de referencia prueba la clase Documento para acceso aleatorio, Lector y Escritor. También demostraron su prueba de consulta por segundo , que me inspiró a crear la mía propia.



El repositorio de .NET Core GitHub incluye un conjunto de puntos de referencia similares a los descritos en este artículo. Miré muy de cerca sus pruebas para asegurarme de que yo mismo no estaba cometiendo errores. Los puedes encontrar enSolución de micro-benchmarks .



Jil tiene sus propios puntos de referencia que comparan a Jil , Newtonsoft , Protobuf y ServiceStack .



Utf8Json ha publicado un conjunto de puntos de referencia disponibles en GitHub . También prueban serializadores binarios.



Alois Kraus ha realizado excelentes pruebas en profundidad de los serializadores .NET más populares, incluidos serializadores JSON, serializadores binarios y serializadores XML. Su punto de referencia incluye puntos de referencia para .NET Core 3 y .NET Framework 4.8.






Obtenga más información sobre el curso.






Lee mas:






All Articles