Le .NET Core 3 récemment publié apporte un certain nombre d'innovations. En plus de C # 8 et de la prise en charge de WinForms et WPF, la dernière version a ajouté un nouveau (dé) sérialiseur JSON - System.Text.Json , et comme son nom l'indique, toutes ses classes sont dans cet espace de noms.
C'est une innovation majeure. La sérialisation JSON est un facteur important dans les applications Web.La plupart des API REST actuelles en dépendent. Lorsque votre client javascript envoie JSON dans le corps d'une requête POST, le serveur utilise la désérialisation JSON pour le convertir en objet C #. Et lorsque le serveur renvoie un objet en réponse, il sérialise cet objet en JSON afin que votre client javascript puisse le comprendre. Ce sont des opérations importantes qui sont effectuées sur chaque requête avec des objets. Leurs performances peuvent affecter considérablement les performances des applications, ce que je vais maintenant démontrer.
Si vous avez de l'expérience avec .NET, vous devez avoir entendu parler de l'excellent sérialiseur Json.NET , également connu sous le nom de Newtonsoft.Json . Alors pourquoi avons-nous besoin d'un nouveau sérialiseur alors que nous avons déjà le joli Newtonsoft.Json ? Bien que Newtonsoft.Json soit sans aucun doute génial, il existe de bonnes raisons de le remplacer:
- Microsoft était désireux d'utiliser de nouveaux types, tels que
, pour améliorer les performances. Modifier une énorme bibliothèque comme Newtonsoft sans casser la fonctionnalité est très difficile.Span<
T> - , HTTP, UTF-8.
String
.NET — UTF-16. Newtonsoft UTF-8 UTF-16, . UTF-8. - Newtonsoft , .NET Framework ( BCL FCL), . ASP.NET Core Newtonsoft, .
Dans cet article, nous allons exécuter quelques tests de performance pour voir à quel point le nouveau sérialiseur est meilleur en termes de performances. En outre, nous comparerons également Newtonsoft.Json et System.Text.Json avec d'autres sérialiseurs bien connus et verrons comment ils se gèrent les uns par rapport aux autres.
Bataille des sérialiseurs
Voici notre règle:
- Newtonsoft.Json (également connu sous le nom de Json.NET ) est un sérialiseur actuellement la norme de l'industrie. Il a été intégré à ASP.NET, bien qu'il s'agisse d'un tiers. Package NuGet n ° 1 de tous les temps. Une bibliothèque multi-primée (je ne sais probablement pas avec certitude).
- 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 est un autre sérialiseur C # vers JSON auto-proclamé le plus rapide. Fonctionne avec une allocation de mémoire nulle et lit / écrit directement dans le binaire UTF8 pour de meilleures performances.
Notez qu'il existe des sérialiseurs non JSON qui sont plus rapides. En particulier, protobuf-net est un sérialiseur binaire qui devrait être plus rapide que n'importe lequel des sérialiseurs comparés dans cet article (qui n'a cependant pas été testé par des benchmarks).
Structure de référence
Les sérialiseurs ne sont pas faciles à comparer. Nous devrons comparer la sérialisation et la désérialisation. Nous aurons besoin de comparer différents types de classes (petites et grandes), listes et dictionnaires. Et nous devrons comparer différentes cibles de sérialisation: chaînes, flux et tableaux de caractères (tableaux UTF-8). Il s'agit d'une matrice de tests assez volumineuse, mais je vais essayer de la garder aussi organisée et concise que possible.
Nous allons tester 4 fonctionnalités différentes:
- Sérialisation en chaîne
- Sérialisation vers un flux
- Désérialiser de la chaîne
- Requêtes par seconde sur l'application ASP.NET Core 3
Pour chacun, nous testerons différents types d'objets (que vous pouvez voir sur GitHub ):
- Petite classe avec seulement 3 propriétés de type primitif.
- Grande classe avec environ 25 propriétés, DateTime et quelques énumérations
- Liste de 1000 éléments (petite classe)
- Dictionnaire de 1000 éléments (petite classe)
Ce ne sont pas tous des repères nécessaires, mais, à mon avis, ils suffisent pour se faire une idée générale.
Pour tous les points de repère je BenchmarkDotNet sur le système suivant: 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
. Vous pouvez trouver le projet de référence lui-même sur GitHub .
Tous les tests ne s'exécuteront que sur les projets .NET Core 3.
Benchmark 1: sérialisation en chaîne
La première chose que nous vérifierons est de sérialiser notre échantillon d'objets en une chaîne.
Le code de référence lui-même est assez simple (voir sur 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 classe de test ci-dessus est générique, nous pouvons donc tester tous nos objets avec le même code, par exemple:
BenchmarkRunner.Run<SerializeToString<Models.BigClass>>();
Après avoir exécuté toutes les classes de test avec tous les sérialiseurs, nous avons obtenu les résultats suivants:
Des indicateurs plus précis peuvent être trouvés ici
- Utf8Json est le plus rapide à ce jour, plus de 4 fois plus rapide que Newtonsoft.Json et System.Text.Json . C'est une différence frappante.
- Jil est également très rapide, environ 2,5 fois plus rapide que Newtonsoft.Json et System.Text.Json.
- Dans la plupart des cas, le nouveau sérialiseur System.Text.Json fonctionne mieux que Newtonsoft.Json d'environ 10%, sauf pour Dictionary, où il a fini par être 10% plus lent.
- L'ancien DataContractJsonSerializer est bien pire que les autres.
- ServiceStack se trouve juste au milieu, montrant qu'il n'est plus le sérialiseur de texte le plus rapide. Au moins pour JSON.
Benchmark 2: sérialisation en streaming
Le deuxième ensemble de tests est à peu près le même, sauf que nous sérialisons dans un flux. Le code de référence est ici . Résultats:
Des chiffres plus précis peuvent être trouvés ici . Merci à Adam Sitnik et Ahson Khan de m'avoir aidé à faire fonctionner System.Text.Json.
Les résultats sont très similaires à ceux du test précédent. Utf8Json et Jil sont 4 fois plus rapides que les autres. Jil est très rapide, juste derrière Utf8Json . Le DataContractJsonSerializer est toujours le plus lent dans la plupart des cas. Newtonsoft fonctionne un peu comme System.Text.Json dans la plupart des cas , sauf pour les dictionnaires où Newtonsoft a un avantage notable .
Benchmark 3: désérialisation à partir d'une chaîne
Le prochain ensemble de tests traite de la désérialisation d'une chaîne. Le code de test peut être trouvé ici .
Des chiffres plus précis peuvent être trouvés ici .
J'ai du mal à exécuter DataContractJsonSerializer pour ce benchmark, il n'est donc pas inclus dans les résultats. Sinon, on voit que Jil est le plus rapide en désérialisation , Utf8Json est à la deuxième place. Ils sont 2 à 3 fois plus rapides que System.Text.Json . Et System.Text.Json est environ 30% plus rapide que Json.NET .
Jusqu'à présent, il s'avère que le populaire Newtonsoft.Json et le nouveau System.Text.Json ont des performances nettement inférieures à celles de leurs concurrents. Ce fut un résultat plutôt inattendu pour moi en raison de la popularité de Newtonsoft.Jsonet tout le battage médiatique autour du nouveau meilleur Microsoft System.Text.Json . Vérifions-le dans une application ASP.NET.
Benchmark 4: le nombre de requêtes par seconde sur le serveur .NET
Comme mentionné précédemment, la sérialisation JSON est très importante car elle est constamment présente dans les API REST. Les requêtes HTTP adressées à un serveur utilisant le type de contenu
application/json
devront sérialiser ou désérialiser l' objet JSON. Lorsque le serveur accepte la charge utile dans une requête POST, le serveur se désérialise de JSON. Lorsque le serveur renvoie un objet dans sa réponse, il sérialise JSON. La communication client-serveur moderne repose fortement sur la sérialisation JSON. Par conséquent, pour tester le scénario «réel», il est logique de créer un serveur de test et de mesurer ses performances.
J'ai été inspiré par le test de performance de Microsoftdans lequel ils ont créé une application serveur MVC et vérifié les demandes par seconde. Microsoft teste System.Text.Json et Newtonsoft.Json . Dans cet article, nous ferons de même, sauf que nous allons les comparer à Utf8Json , qui s'est avéré être l'un des sérialiseurs les plus rapides lors des tests précédents.
Malheureusement, je n'ai pas pu intégrer ASP.NET Core 3 avec Jil, donc le benchmark ne l'inclut pas. Je suis presque sûr que cela peut être fait avec plus d'efforts, mais hélas.
La création de ce test s'est avérée plus difficile qu'avant. J'ai d'abord créé une application MVC ASP.NET Core 3.0, tout comme dans le benchmark Microsoft. J'ai ajouté un contrôleur pour les tests de performances, similaire à celui du test 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;
}
Lorsque le client appelle le point de terminaison
DeserializeThousandSmallClassList
, le serveur accepte le texte JSON et désérialise le contenu. C'est ainsi que nous testons la désérialisation. Lorsque le client appelle SerializeThousandSmallClassList
, le serveur renvoie une liste de 1000 SmallClass
éléments et sérialise ainsi le contenu en JSON.
Ensuite, nous devons annuler la journalisation pour chaque requête afin qu'elle n'affecte pas le résultat:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureLogging(logging =>
{
logging.ClearProviders();
//logging.AddConsole();
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
Nous avons maintenant besoin d'un moyen de basculer entre System.Text.Json , Newtonsoft et Utf8Json . C'est facile avec les deux premiers. Pour System.Text.Json, vous n'avez rien à faire du tout. Pour passer à Newtonsoft.Json, ajoutez simplement une ligne à
ConfigureServices
:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews()
// Newtonsoft. - System.Text.Json
.AddNewtonsoftJson()
;
Pour Utf8Json, nous devons ajouter des formateurs de médias personnalisés
InputFormatter
et des fichiersOutputFormatter
. Ce n'était pas si simple, mais à la fin j'ai trouvé une bonne solution sur Internet , et après avoir fouillé dans les paramètres, cela a fonctionné. Il existe également un package NuGet avec des formateurs, mais il ne fonctionne pas avec 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);
}
}
}
Maintenant, pour ASP.NET pour utiliser ces formateurs:
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());
});
}
Voici donc le serveur. Maintenant sur le client.
Client C # pour mesurer les demandes par seconde
J'ai également créé une application cliente C #, bien que les clients JavaScript prévaudront dans la plupart des scénarios du monde réel. Cela n'a pas d'importance pour nos objectifs. Voici le code:
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();
}
}
Ce client enverra continuellement des demandes pendant 1 seconde, en les comptant.
résultats
Alors, sans plus tarder, voici les résultats:
Des indicateurs plus précis peuvent être trouvés ici
Utf8Json a surpassé les autres sérialiseurs par une marge énorme. Ce n'était pas une grosse surprise après les tests précédents.
En termes de sérialisation, Utf8Json est 2x plus rapide que System.Text.Json et 4x plus rapide que Newtonsoft . Pour la désérialisation, Utf8Json est 3,5 fois plus rapide que System.Text.Json et 6 fois plus rapide que Newtonsoft .
La seule surprise pour moi ici est à quel point Newtonsoft.Json fonctionne mal... Cela est probablement dû au problème UTF-16 et UTF-8. Le protocole HTTP fonctionne avec du texte UTF-8. Newtonsoft convertit ce texte en types de chaîne .NET, qui sont UTF-16. Cette surcharge n'est présente ni dans Utf8Json ni dans System.Text.Json , qui fonctionnent directement avec UTF-8.
Il est important de noter que ces points de repère ne doivent pas être fiables à 100% car ils peuvent ne pas refléter pleinement le scénario réel. Et c'est pourquoi:
- J'ai tout exécuté sur ma machine locale - client et serveur. Dans un scénario réel, le serveur et le client sont sur des machines différentes.
- . , . . - . , , . , , GC. Utf8Json, .
- Microsoft ( 100 000). , , , , .
- . , - - .
Tout bien considéré, ces résultats sont assez incroyables. Il semble que le temps de réponse puisse être considérablement amélioré en choisissant le bon sérialiseur JSON. Le passage de Newtonsoft à System.Text.Json augmentera le nombre de requêtes de 2 à 7 fois, et le passage de Newtonsoft à Utf8Json s'améliorera de 6 à 14 fois. Ce n'est pas tout à fait juste, car un vrai serveur fera bien plus que simplement accepter des arguments et renvoyer des objets. Il fera probablement également d'autres choses, comme travailler avec des bases de données et donc exécuter une logique métier, de sorte que le temps de sérialisation peut être moins important. Cependant, ces chiffres sont incroyables.
conclusions
Résumons:
- System.Text.Json , Newtonsoft.Json ( ). Microsoft .
- , Newtonsoft.Json System.Text.Json. , Utf8Json Jil 2-4 , System.Text.Json.
- , Utf8Json ASP.NET . , , , ASP.NET.
Cela signifie-t-il que nous devrions tous passer à Utf8Json ou Jil? La réponse à cela est ... peut-être. N'oubliez pas que Newtonsoft.Json a résisté à l'épreuve du temps et est devenu le sérialiseur le plus populaire pour une raison. Il prend en charge de nombreuses fonctionnalités, a été testé avec tous les types de cas extrêmes et dispose de tonnes de solutions et de solutions de contournement documentées. Les deux System.Text.Json et Newtonsoft.Json sont très bien pris en charge. Microsoft continuera à investir des ressources et des efforts dans System.Text.Json afin que vous puissiez compter sur un excellent support. Alors que Jil et Utf8Jsonont reçu très peu d'engagements l'année dernière. En fait, il semble qu'ils n'ont pas eu beaucoup d'entretien au cours des 6 derniers mois.
Une option consiste à combiner plusieurs sérialiseurs dans votre application. Passez à des sérialiseurs plus rapides pour l'intégration ASP.NET pour des performances supérieures, mais continuez à utiliser Newtonsoft.Json dans votre logique métier pour tirer le meilleur parti de son ensemble de fonctionnalités.
J'espère que vous avez apprécié cet article. Bonne chance)
Autres références
Plusieurs autres benchmarks comparant divers sérialiseurs
Lorsque Microsoft a annoncé System.Text.Json, ils ont montré leur propre benchmark comparant System.Text.Json et Newtonsoft.Json . En plus de la sérialisation et de la désérialisation, ce test teste la classe Document pour l'accès aléatoire, Reader et Writer. Ils ont également présenté leur test Query Per Second , qui m'a inspiré à créer le mien.
Le référentiel .NET Core GitHub comprend un ensemble de tests de performance similaires à ceux décrits dans cet article. J'ai regardé de très près leurs tests pour m'assurer que je ne faisais pas d'erreur moi-même. Vous pouvez les trouver dansSolution de micro-benchmarks .
Jil a ses propres benchmarks qui comparent Jil , Newtonsoft , Protobuf et ServiceStack .
Utf8Json a publié un ensemble de benchmarks disponibles sur GitHub . Ils testent également les sérialiseurs binaires.
Alois Kraus a réalisé d'excellents tests approfondis sur les sérialiseurs .NET les plus populaires, y compris les sérialiseurs JSON, les sérialiseurs binaires et les sérialiseurs XML. Son benchmark comprend des benchmarks pour .NET Core 3 et .NET Framework 4.8.
En savoir plus sur le cours.