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

时间:2019-06-05 12:59:57

标签: c# json.net deserialization benchmarkdotnet

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

我知道Newtonsoft.Json's Performance Tips使用流,但是我想知道更多并且有硬数字。我已经使用BenchmarkDotNet编写了简单的基准测试,但是我对结果感到困惑(请参见下面的数字)。

我得到了什么

  • 从流中解析总是更快,但并不是很多
  • 使用字符串作为输入时,解析小型和“中型” JSON具有更好或相等的内存使用率
  • 使用大型JSON(字符串本身最终出现在LOH中)开始看到内存使用率的显着差异

我还没有时间做适当的分析(还),我对使用流方法的内存开销感到惊讶(如果没有错误)。整个代码为here

  • 我的方法正确吗? ("stylePreprocessorOptions": { "includePaths": [ "./src/styles" ] } 的使用;模拟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)];
}

字符串反序列化

我们首先从流读取JSON到字符串,然后运行反序列化-正在分配另一个字符串,然后将其用于反序列化。

[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);
        }
    }
}

常用方法

[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);
    }
}

结果

小JSON

重复10000次

  • 流:平均25.69 ms,已分配61.34 MB
  • 字符串:平均31.22 ms,已分配36.01 MB

中等JSON

重复1000次

  • 流:平均24.07毫秒,已分配12 MB
  • 字符串:平均25.09毫秒,已分配12.85 MB

大JSON

重复了100次

  • 流:平均229.6 ms,已分配47.54 MB,对象已到达第1代
  • 字符串:平均240.8毫秒,已分配92.42 MB,对象已达到第二代!

更新

我深入研究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); 的来源,发现从JsonConvertJsonConvert:816反序列化时,它内部使用JsonTextReaderStringReader。流也在那里(当然!)。

然后,我决定进一步研究string本身,一见钟情-总是分配数组缓冲区(StreamReader):StreamReader:244,这解释了它的内存使用情况。

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

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

1 个答案:

答案 0 :(得分:2)

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

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

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