基准测试Newtonsoft.Json反序列化:从流和从字符串



我对如何使用Newtonsoft.Json反序列化HTTP响应JSON有效负载的两种方法的性能(速度,内存使用情况)比较感兴趣。

我知道Newtonsoft.Json使用流的性能提示,但我想了解更多并拥有硬数字。我已经使用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,对象到达第 1 代
  • 字符串:平均 240.8 毫秒,分配 92.42 MB,对象到达第 2 代!

更新

我通过JsonConvert源代码,发现它在从string反序列化时内部使用JsonTextReaderStringReader:JsonConvert:816。那里也涉及流(当然!

然后我决定深入研究StreamReader本身,第一眼就惊呆了 - 它总是分配数组缓冲区(byte[]):StreamReader:244,这解释了它的内存使用。

这给了我"为什么"的答案。解决方案很简单 - 实例化StreamReader时使用较小的缓冲区大小 - 最小缓冲区大小默认为 128(请参阅StreamReader.MinBufferSize),但您可以> 0提供任何值(检查 ctor 重载之一)。

当然,缓冲区大小处理数据有影响。回答我应该使用什么缓冲区大小:这取决于。当期望较小的 JSON 响应时,我认为坚持使用小缓冲区是安全的。

经过一番摆弄,我发现了使用StreamReader时内存分配背后的原因。原始帖子已更新,但在此处回顾:

StreamReader使用设置为 1024 的默认bufferSize。然后,StreamReader的每个实例化都会分配该大小的字节数组。这就是为什么我在基准测试中看到这些数字的原因。

当我bufferSize设置为其最低可能值128时,结果似乎要好得多。

  • 流读取器:244,初始化
  • StreamReader.MinBufferSize

相关内容

  • 没有找到相关文章

最新更新