我对使用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,对象到达第一代
- 字符串:平均240.8毫秒,分配92.42 MB,对象到达第二代!
更新
我查看了JsonConvert
的源代码,并发现它在从string
反序列化时内部使用JsonTextReader
和StringReader
:JsonConvert:816。当然,流也参与其中。
然后我决定深入研究StreamReader
本身,一眼就被惊呆了——它总是分配数组缓冲区(byte[]
):StreamReader:244,这解释了它的内存使用情况。
这给了我答案"为什么"。解决方案很简单——在实例化StreamReader
时使用较小的缓冲区大小——最小缓冲区大小默认为128(请参阅StreamReader.MinBufferSize
),但您可以提供任何值>0
(检查一个构造函数重载)。
_memory
流被复制到另一个内存流中?http://www.tugberkugurlu.com/archive/efficiently-streaming-large-http-responses-with-httpclient 可能是相关的。 - dbcStreamReader
的作用。更新了问题(...并提出了可能的解决方案)。 - Zdeněk