4

Newtonsoft.Jsonを使用して HTTP 応答 JSON ペイロードを逆シリアル化する 2 つのアプローチのパフォーマンス (速度、メモリ使用量) の比較に興味があります。

ストリームを使用するためのNewtonsoft.Json の Performance Tipsは知っていますが、もっと知りたいと思っていました。BenchmarkDotNetを使用して簡単なベンチマークを作成しましたが、結果に少し困惑しています (以下の数値を参照)。

私が得たもの:

  • ストリームからの解析は常に高速ですが、それほど多くはありません
  • 文字列を入力として使用すると、小規模および「中規模」の JSON を解析すると、より良いまたは同等のメモリ使用量になります。
  • メモリ使用量の大きな違いは、大規模な JSON で見られ始めます (文字列自体が LOH になる)。

(まだ) 適切なプロファイリングを行う時間がありませんでした。(エラーがない場合) ストリーム アプローチのメモリ オーバーヘッドに少し驚いています。コード全体はこちらです。

?

  • 私のアプローチは正しいですか?(使用法MemoryStream; シミュレートHttpResponseMessageとその内容; ...)
  • ベンチマーク コードに問題はありますか?
  • なぜこのような結果が表示されるのですか?

ベンチマークの設定

MemoryStreamベンチマークの実行中に何度も使用する準備をしています:

[GlobalSetup]
public void GlobalSetup()
{
    var resourceName = _resourceMapping[typeof(T)];
    using (var resourceStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName))
    {
        _memory = new MemoryStream();
        resourceStream.CopyTo(_memory);
    }

    _iterationRepeats = _repeatMapping[typeof(T)];
}

ストリームの逆シリアル化

[Benchmark(Description = "Stream d13n")]
public async Task DeserializeStream()
{
    for (var i = 0; i < _iterationRepeats; i++)
    {
        var response = BuildResponse(_memory);

        using (var streamReader = BuildNonClosingStreamReader(await response.Content.ReadAsStreamAsync()))
        using (var jsonReader = new JsonTextReader(streamReader))
        {
            _serializer.Deserialize<T>(jsonReader);
        }
    }
}

文字列の逆シリアル化

最初に JSON をストリームから文字列に読み取り、次に逆シリアル化を実行します。別の文字列が割り当てられた後、逆シリアル化に使用されます。

[Benchmark(Description = "String d13n")]
public async Task DeserializeString()
{
    for (var i = 0; i < _iterationRepeats; i++)
    {
        var response = BuildResponse(_memory);

        var content = await response.Content.ReadAsStringAsync();
        JsonConvert.DeserializeObject<T>(content);
    }
}

一般的な方法

private static HttpResponseMessage BuildResponse(Stream stream)
{
    stream.Seek(0, SeekOrigin.Begin);

    var content = new StreamContent(stream);
    content.Headers.ContentType = new MediaTypeHeaderValue("application/json");

    return new HttpResponseMessage(HttpStatusCode.OK)
    {
        Content = content
    };
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static StreamReader BuildNonClosingStreamReader(Stream inputStream) =>
    new StreamReader(
        stream: inputStream,
        encoding: Encoding.UTF8,
        detectEncodingFromByteOrderMarks: true,
        bufferSize: 1024,
        leaveOpen: true);

結果

小さな JSON

10000回繰り返し

  • ストリーム: 平均 25.69 ミリ秒、61.34 MB 割り当て
  • 文字列: 平均 31.22 ミリ秒、割り当てられた 36.01 MB

ミディアム JSON

1000回繰り返し

  • ストリーム: 平均 24.07 ミリ秒、12 MB 割り当て
  • 文字列: 平均 25.09 ミリ秒、割り当てられた 12.85 MB

大きな JSON

100回繰り返した

  • ストリーム: 平均 229.6 ミリ秒、47.54 MB 割り当て、オブジェクトは Gen 1 に到達
  • 文字列: 平均 240.8 ミリ秒、割り当てられた 92.42 MB、オブジェクトが Gen 2 に到達しました!

アップデート

のソースを調べたところ、 JsonConvert:816から逆シリアル化するときにwith をJsonConvert内部的に使用していることがわかりました。ストリームもそこに関与しています (もちろん!)。JsonTextReaderStringReaderstring

次に、それ自体をさらに掘り下げることにしましたStreamReaderが、一目惚れしました-常に配列 buffer ( byte[]): StreamReader:244を割り当てています。これは、そのメモリ使用を説明しています。

これで「なぜ」の答えが得られます。解決策は簡単です。インスタンス化するときに小さいバッファ サイズを使用します。StreamReader最小バッファ サイズのデフォルトは 128 ですが (「参考文献」を参照StreamReader.MinBufferSize)、任意の値を指定できます> 0(ctor オーバーロードの 1 つを確認してください)。

もちろん、バッファサイズデータの処理に影響します。次に使用する必要があるバッファ サイズに答えると、依存します。より小さな JSON 応答が予想される場合は、小さなバッファーに固執するのが安全だと思います。

4

1 に答える 1