基准测试 Newtonsoft.Json 反序列化:来自流和来自字符串
Benchmarking Newtonsoft.Json deserialization: from stream and from string
我对如何使用 Newtonsoft.Json.
反序列化 HTTP 响应 JSON 负载的两种方法的性能(速度、内存使用)比较感兴趣
我知道 Newtonsoft.Json's Performance Tips to use streams, but I wanted to know more and have hard numbers. I've written simple benchmark using BenchmarkDotNet,但我对结果有点困惑(见下面的数字)。
我得到了什么:
- 从流中解析总是更快,但实际上并不多
- 解析小而 "medium" JSON 在使用字符串作为输入时具有更好或相等的内存使用率
- 大 JSON(其中字符串本身以 LOH 结束)
开始出现显着的内存使用差异
我还没有时间进行适当的分析,我对流方法的内存开销感到有点惊讶(如果没有错误的话)。整个代码是 here.
?
- 我的做法正确吗? (使用
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
反序列化时在内部使用 JsonTextReader
和 StringReader
:JsonConvert:816。 Stream 也参与其中(当然!)。
然后我决定更深入地研究 StreamReader
本身,第一眼我惊呆了——它总是在分配数组缓冲区 (byte[]
): StreamReader:244,这解释了它的内存使用。
这给了我 "why" 的答案。解决方案很简单 - 在实例化 StreamReader
时使用较小的缓冲区大小 - 最小缓冲区大小默认为 128(请参阅 StreamReader.MinBufferSize
),但您可以提供任何值 > 0
(检查 ctor 重载之一)。
当然缓冲区大小对处理数据有影响。回答我应该使用什么缓冲区大小:这取决于。当期望较小的 JSON 响应时,我认为坚持使用小缓冲区是安全的。
经过一些摆弄后,我发现了使用 StreamReader
时内存分配背后的原因。原 post 已更新,但在此重述:
StreamReader
使用默认 bufferSize
设置为 1024。 StreamReader
的每个实例然后分配该大小的字节数组。这就是我在基准测试中看到这些数字的原因。
当我将 bufferSize
设置为可能的最低值 128
时,结果似乎好多了。
我对如何使用 Newtonsoft.Json.
反序列化 HTTP 响应 JSON 负载的两种方法的性能(速度、内存使用)比较感兴趣我知道 Newtonsoft.Json's Performance Tips to use streams, but I wanted to know more and have hard numbers. I've written simple benchmark using BenchmarkDotNet,但我对结果有点困惑(见下面的数字)。
我得到了什么:
- 从流中解析总是更快,但实际上并不多
- 解析小而 "medium" JSON 在使用字符串作为输入时具有更好或相等的内存使用率
- 大 JSON(其中字符串本身以 LOH 结束) 开始出现显着的内存使用差异
我还没有时间进行适当的分析,我对流方法的内存开销感到有点惊讶(如果没有错误的话)。整个代码是 here.
?
- 我的做法正确吗? (使用
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
反序列化时在内部使用 JsonTextReader
和 StringReader
:JsonConvert:816。 Stream 也参与其中(当然!)。
然后我决定更深入地研究 StreamReader
本身,第一眼我惊呆了——它总是在分配数组缓冲区 (byte[]
): StreamReader:244,这解释了它的内存使用。
这给了我 "why" 的答案。解决方案很简单 - 在实例化 StreamReader
时使用较小的缓冲区大小 - 最小缓冲区大小默认为 128(请参阅 StreamReader.MinBufferSize
),但您可以提供任何值 > 0
(检查 ctor 重载之一)。
当然缓冲区大小对处理数据有影响。回答我应该使用什么缓冲区大小:这取决于。当期望较小的 JSON 响应时,我认为坚持使用小缓冲区是安全的。
经过一些摆弄后,我发现了使用 StreamReader
时内存分配背后的原因。原 post 已更新,但在此重述:
StreamReader
使用默认 bufferSize
设置为 1024。 StreamReader
的每个实例然后分配该大小的字节数组。这就是我在基准测试中看到这些数字的原因。
当我将 bufferSize
设置为可能的最低值 128
时,结果似乎好多了。