.NET Core 3用のC#JSONシリアライザーの戦い

こんにちは。「C#Developer」コースの開始を見越し、おもしろい翻訳を用意しました。また、無料でレッスン記録「Design Pattern State(State)」をご覧いただくこともできます










最近リリースされた.NETCore 3は、多くの革新をもたらしました。 C#8とWinFormsおよびWPFのサポートに加えて、最新リリースでは新しいJSON(de)シリアライザーSystem.Text.Jsonが追加されており、その名前が示すように、そのクラスはすべてこの名前名にあります。



これは大きな革新です。JSONシリアル化は、Webアプリケーションの重要な要素です。今日のRESTAPIのほとんどはそれに依存しています。 javascriptクライアントがPOSTリクエストの本文でJSONを送信すると、サーバーはJSON逆シリアル化を使用してそれをC#オブジェクトに変換します。また、サーバーが応答としてオブジェクトを返すと、javascriptクライアントが理解できるように、このオブジェクトをJSONにシリアル化します。これらは、オブジェクトを使用するすべての要求に対して実行される大規模な操作です。それらのパフォーマンスは、アプリケーションのパフォーマンスに大きな影響を与える可能性があります。これについては、これから説明します。



.NETの使用経験がある場合は、Newtonsoft.Jsonとしても知られる優れたJson.NETシリアライザーについて聞いたことがあるはずです。では、素敵な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.JsonSystem.Text.Jsonを他のよく知られたシリアライザー比較し、それらが相互にどのように処理されるかを確認します。



シリアライザーの戦い



これが私たちの支配者です:



  • Newtonsoft.JsonJson.NETとも呼ばれます)は、現在業界標準のシリアライザーです。サードパーティでしたが、ASP.NETに統合されました。史上最高のNuGetパッケージ。複数の賞を受賞したライブラリ(私はおそらく確かに知りません)。
  • 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は、JSONシリアライザーに対するもう1つの自称最速のC#です。ゼロメモリ割り当てで動作し、パフォーマンスを向上させるためにUTF8バイナリに直接読み取り/書き込みします。





より高速な非JSONシリアライザーがあることに注意してください。特に、protobuf-netはバイナリシリアライザーであり、この記事で比較したどのシリアライザーよりも高速である必要があります(ただし、ベンチマークではテストされていません)。



ベンチマーク構造



シリアライザーを比較するのは簡単ではありません。シリアル化と逆シリアル化を比較する必要があります。さまざまなタイプのクラス(小規模および大規模)、リスト、および辞書を比較する必要があります。また、文字列、ストリーム、文字配列(UTF-8配列)など、さまざまなシリアル化ターゲットを比較する必要があります。これはかなり大きなテストマトリックスですが、できるだけ整理して簡潔にするように努めます。



4つの異なる機能をテストします。



  • 文字列へのシリアル化
  • ストリームへのシリアル化
  • 文字列から逆シリアル化
  • ASP.NET Core3アプリでの1秒あたりのリクエスト数


それぞれについて、さまざまなタイプのオブジェクトをテストします(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 Core3プロジェクトでのみ実行されます。



ベンチマーク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はこれまでで最速であり、Newtonsoft.JsonおよびSystem.Text.Jsonよりも4倍以上高速ですこれは著しい違いです。
  • Jilも非常に高速で、Newtonsoft.JsonおよびSystem.Text.Jsonよりも約2.5倍高速です
  • ほとんどの場合、新しいSystem.Text.JsonシリアライザーNewtonsoft.Jsonよりも約10%パフォーマンスが優れていますが、Dictionaryは10%遅くなりました。
  • 古いDataContractJsonSerializerは他のものよりはるかに悪いです。
  • ServiceStackは真ん中に位置し、最速のテキストシリアライザーではなくなったことを示しています。少なくともJSONの場合。


ベンチマーク2:ストリームへのシリアル化



テストの2番目のセットは、ストリームにシリアル化することを除いて、ほとんど同じです。ベンチマークコードはこちらです。結果:







より正確な数値はここにありますSystem.Text.Jsonを機能させるのを手伝ってくれたAdamSitnikとAhsonKhanに感謝します。


結果は前のテストと非常に似ています。Utf8JsonJilは他の4倍高速です。Jilは非常に高速で、Utf8Jsonに次ぐものです。DataContractJsonSerializerは、ほとんどの場合、依然として最も低速です。Newtonsoftは、Newtonsoftに顕著な利点がある辞書を除いて、ほとんどの場合System.Text.Jsonとほぼ同じように機能します



ベンチマーク3:文字列からの逆シリアル化



次の一連のテストでは、文字列からの逆シリアル化を扱います。テストコードはここにあります







より正確な数値はここにあります


このベンチマークでDataContractJsonSerializer 実行するのに問題があるため、結果に含まれていません。それ以外の場合は、Jilが逆シリアル化で最速でありUtf8Jsonが2位であることがわかります。System.Text.Jsonより2〜3倍高速です。また、System.Text.JsonはJson.NETよりも約30%高速です



これまでのところ、人気のあるNewtonsoft.Jsonと新しいSystem.Text.Jsonのパフォーマンスは、競合他社よりも大幅に劣っています。Newtonsoft.Jsonの人気により、これは私にとってかなり予想外の結果でした。そして新しいトップパフォーマーマイクロソフトの周りのすべての誇大宣伝System.Text.JsonASP.NETアプリケーションで確認してみましょう。



ベンチマーク4:.NETサーバーでの1秒あたりのリクエスト数



前述のように、JSONシリアル化はREST APIに常に存在するため、非常に重要です。コンテンツタイプapplication/json使用するサーバーへのHTTP要求は、 JSONオブジェクトをシリアル化または逆シリアル化する必要があります。サーバーがPOST要求でペイロードを受け入れると、サーバーはJSONから逆シリアル化します。サーバーが応答でオブジェクトを返すと、JSONをシリアル化します。最新のクライアントとサーバーの通信は、JSONシリアル化に大きく依存しています。したがって、「実際の」シナリオをテストするには、テストサーバーを作成してそのパフォーマンスを測定するのが理にかなっています。マイクロソフトのパフォーマンステスト



に触発されましたそこで彼らはMVCサーバーアプリケーションを作成し、1秒あたりのリクエストをチェックしました。MicrosoftベンチマークはSystem.Text.JsonNewtonsoft.Jsonをテストしますこの記事では、以前のテストで最速のシリアライザーの1つであることが証明されているUtf8Jsonと比較することを除いて、同じことを行います。

残念ながら、ASP.NET Core 3をJilと統合できなかったため、ベンチマークには含まれていません。もっと努力すればできると確信していますが、残念ながら。


このテストの作成は、以前よりも困難であることが判明しました。私は最初に、Microsoftベンチマークと同じように、ASP.NET Core 3.0MVCアプリを作成しました。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;
}


クライアントがエンドポイントを呼び出すDeserializeThousandSmallClassListと、サーバーはJSONテキストを受け入れ、コンテンツを逆シリアル化します。これが、逆シリアル化をテストする方法です。クライアントがを呼び出すSerializeThousandSmallClassListと、サーバーは1000SmallClassアイテムのリストを返し、それによってコンテンツをJSONにシリアル化します。



次に、結果に影響を与えないように、各リクエストのログをキャンセルする必要があります。



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


System.Text.JsonNewtonsoftUtf8Json を切り替える方法が必要です最初の2つで、それは簡単です。System.Text.Jsonあなたはまったく何もする必要はありません。Newtonsoft.Jsonに切り替えるには、次の行に1行追加するだけConfigureServices です。



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


Utf8Json、我々は、カスタムメディアフォーマッタを追加する必要がありますInputFormatterOutputFormatterそれほど簡単ではありませんでしたが、最終的にはインターネット良い解決策を見つけ、設定を調べた後うまくいきましたフォーマッタを含むNuGetパッケージもありますが、ASP.NET Core3では機能しません。



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#1秒あたりのリクエストを測定するためのクライアント



また、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は、他のシリアライザーを大幅に上回りました。以前のテストの後、これは大きな驚きではありませんでした。



シリアル化に関しては、Utf8JsonSystem.Text.Jsonより2倍高速でNewtonsoftより4倍高速です。デシリアライズの場合、Utf8JsonSystem.Text.Jsonの3.5倍Newtonsoftの6倍高速です



ここでの私にとっての唯一の驚きは、Newtonsoft.Jsonの動作がいかに貧弱かということです。..。これはおそらくUTF-16とUTF-8の問題が原因です。HTTPプロトコルはUTF-8テキストで機能します。Newtonsoftは、このテキストをUTF-16である.NET文字列タイプに変換します。このオーバーヘッドは、UTF-8と直接連携するUtf8JsonまたはSystem.Text.Jsonのいずれにも存在しません



これらのベンチマークは、実際のシナリオを完全に反映していない可能性があるため、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.JsonNewtonsoft.Jsonは非常によくサポート。マイクロソフトは引き続きSystem.Text.Jsonにリソースと労力を投資して、優れたサポートを期待できるようにします。一方、ジルUtf8Json昨年はほとんどコミットを受け取りませんでした。実際、過去6か月間はあまりメンテナンスが行われていないようです。



1つのオプションは、アプリケーションで複数のシリアライザーを組み合わせることです。より高速なシリアライザーアップグレードしてASP.NETと統合し、優れたパフォーマンスを実現しますが、ビジネスロジックでNewtonsoft.Json引き続き使用して、機能セット最大限に活用してください。



この記事を楽しんでいただけたでしょうか。幸運を)



その他のベンチマーク



さまざまなシリアライザーを比較する他のいくつかのベンチマーク



MicrosoftSystem.Text.Jsonを発表したときSystem.Text.JsonNewtonsoft.Jsonを比較する独自のベンチマークを示しました。シリアル化と逆シリアル化に加えて、このベンチマークは、ランダムアクセス、リーダー、ライターについてDocumentクラスをテストします。彼らはまた、1秒あたりのクエリテストのデモも行いました .NET Core GitHubリポジトリには、この記事で説明したものと同様の一連のベンチマークが含まれています。私は彼らのテストを非常に綿密に調べて、自分で間違いを犯していないことを確認しました。あなたがそれらを見つけることができます



マイクロベンチマークソリューション



Jilには、JilNewtonsoftProtobuf、およびServiceStackを比較する独自のベンチマークあります。 Utf8Jsonは、GitHubで利用可能な一連のベンチマークを投稿しましたまた、バイナリシリアライザーもテストします。 Alois Krausは、JSONシリアライザー、バイナリシリアライザー、XMLシリアライザーなど最も人気のある.NETシリアライザーの優れた詳細テストを実施しましたそのベンチマークには、.NET Core3および.NETFramework4.8のベンチマークが含まれています。














コースの詳細をご覧ください。






続きを読む:






All Articles