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

7

我对使用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反序列化时内部使用JsonTextReaderStringReaderJsonConvert:816。当然,流也参与其中。

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

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

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

可能存在缓冲和异步处理问题,导致_memory流被复制到另一个内存流中?http://www.tugberkugurlu.com/archive/efficiently-streaming-large-http-responses-with-httpclient 可能是相关的。 - dbc
@dbc 这在我的测试中是预期的(需要在某个地方填充流)。我甚至尝试查看 .NET 代码 - 当将流序列化为字符串时,会发生一些复制 - 但这与我看到的结果相矛盾。(但当然这是一个不错的性能提示!) - Zdeněk
@dbc 我有更多时间了解了StreamReader的作用。更新了问题(...并提出了可能的解决方案)。 - Zdeněk
有趣,谢谢。如果你愿意的话,你可以回答自己的问题 - dbc
https://devblogs.microsoft.com/aspnet/asp-net-core-updates-in-net-core-3-0-preview-5/ 新的JSON库 - Avin Kavish
1个回答

4

经过一些调试,我发现了在使用StreamReader时内存分配的原因。原帖已更新,但这里简要概括一下:

StreamReader默认使用bufferSize,大小为1024。每次实例化StreamReader时都会分配这个大小的字节数组。这就是我在基准测试中看到这样的数字的原因。

当我将bufferSize设置为最小可行值,即128时,结果似乎好多了。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接