【问题标题】:Benchmarking Newtonsoft.Json deserialization: from stream and from string基准测试 Newtonsoft.Json 反序列化:来自流和来自字符串
【发布时间】:2019-10-20 23:53:40
【问题描述】:

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

我知道Newtonsoft.Json's Performance Tips 可以使用流,但我想了解更多信息并获得确切的数字。我使用BenchmarkDotNet 编写了简单的基准测试,但我对结果有点困惑(见下面的数字)。

我得到了什么:

  • 从流中解析总是更快,但不是很快
  • 在使用字符串作为输入时,解析小型和“中型”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 反序列化时在内部使用JsonTextReaderStringReaderJsonConvert:816。流也参与其中(当然!)。

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

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

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

【问题讨论】:

  • 缓冲和async 可能存在一些问题,从而_memory 流被复制到另一个内存流中? tugberkugurlu.com/archive/… 可能是相关的。
  • @dbc 在我的测试中这是预期的(在某处填充流)。我什至尝试过 .NET 代码——将流序列化为字符串时,会发生一些复制——但这与我看到的结果相反。 (但当然这是很好的性能提示!)
  • @dbc 我有更多时间了解StreamReader 做了什么。更新的问题(...和建议的可能解决方案)。
  • 很有趣,谢谢。如果你愿意,你可以answer your own question

标签: c# json.net deserialization benchmarkdotnet


【解决方案1】:

经过一番摆弄,我找到了使用StreamReader 时内存分配背后的原因。原帖已更新,但在这里回顾一下:

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

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

【讨论】:

    猜你喜欢
    • 2018-02-05
    • 1970-01-01
    • 1970-01-01
    • 2021-11-01
    • 1970-01-01
    • 2021-07-19
    • 2015-03-14
    • 1970-01-01
    • 2012-08-11
    相关资源
    最近更新 更多