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
内部的に使用していることがわかりました。ストリームもそこに関与しています (もちろん!)。JsonTextReader
StringReader
string
次に、それ自体をさらに掘り下げることにしましたStreamReader
が、一目惚れしました-常に配列 buffer ( byte[]
): StreamReader:244を割り当てています。これは、そのメモリ使用を説明しています。
これで「なぜ」の答えが得られます。解決策は簡単です。インスタンス化するときに小さいバッファ サイズを使用します。StreamReader
最小バッファ サイズのデフォルトは 128 ですが (「参考文献」を参照StreamReader.MinBufferSize
)、任意の値を指定できます> 0
(ctor オーバーロードの 1 つを確認してください)。
もちろん、バッファサイズはデータの処理に影響します。次に使用する必要があるバッファ サイズに答えると、依存します。より小さな JSON 応答が予想される場合は、小さなバッファーに固執するのが安全だと思います。