Newtonsoft json.net JsonTextReader垃圾收集器密集型

时间:2019-04-23 13:27:26

标签: .net performance garbage-collection json.net

我们正在使用Newtonsoft.Json nuget包使用通过HTTP以JSON序列化为JSON的大型(GB)网络流,将响应流反序列化为内存中的记录以进行进一步处理。

鉴于数据量过多,我们正在使用流技术一次接收大量响应,并希望在达到CPU极限时优化此过程。

JsonTextReader 是进行优化的候选之一,它会不断分配新对象,从而触发垃圾回收。

我们遵循了Newtonsoft Performance Tips的建议。

我创建了一个示例.net控制台应用程序,用于模拟JsonTextReader读取响应流时分配新对象的行为,分配表示属性名称和值的字符串

问题:

>在现实世界中,有95%的重复(在测试中是同一条记录,所以100%重复),我们还能做些其他调整/重写来重用已分配的属性名/值实例吗?

示例应用程序:

Install-Package Newtonsoft.Json -Version 12.0.2
Install-Package System.Buffers -Version 4.5.0

Program.cs

using System;
using System.Buffers;
using System.IO;
using System.Linq;
using System.Text;
using Newtonsoft.Json;

namespace JsonNetTester
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var sr = new MockedStreamReader())
            using (var jtr = new JsonTextReader(sr))
            {
                // does not seem to make any difference
                //jtr.ArrayPool = JsonArrayPool.Instance;

                // every read is allocating new objects
                while (jtr.Read())
                {
                }
            }
        }

        // simulating continuous stream of records serialised as json
        public class MockedStreamReader : StreamReader
        {
            private bool initialProvided = false;
            private byte[] initialBytes = Encoding.Default.GetBytes("[");
            private static readonly byte[] recordBytes;
            int nextStart = 0;

            static MockedStreamReader()
            {
                var recordSb = new StringBuilder("{");

                // generate [i] of { "Key[i]": "Value[i]" }, 
                Enumerable.Range(0, 50).ToList().ForEach(i =>
                {
                    if (i > 0)
                    {
                        recordSb.Append(",");
                    }
                    recordSb.Append($"\"Key{i}\": \"Value{i}\"");
                });

                recordSb.Append("},");
                recordBytes = Encoding.Default.GetBytes(recordSb.ToString());
            }

            public MockedStreamReader() : base(new MemoryStream())
            {   }

            public override int Read(char[] buffer, int index, int count)
            {
                // keep on reading the same record in loop
                if (this.initialProvided)
                {
                    var start = nextStart;
                    var length = Math.Min(recordBytes.Length - start, count);
                    var end = start + length;
                    nextStart = end >= recordBytes.Length ? 0 : end;
                    Array.Copy(recordBytes, start, buffer, index, length);
                    return length;
                }
                else
                {
                    initialProvided = true;
                    Array.Copy(initialBytes, buffer, initialBytes.Length);
                    return initialBytes.Length;
                }
            }
        }

        // attempt to reuse data in serialisation
        public class JsonArrayPool : IArrayPool<char>
        {
            public static readonly JsonArrayPool Instance = new JsonArrayPool();

            public char[] Rent(int minimumLength)
            {
                return ArrayPool<char>.Shared.Rent(minimumLength);
            }

            public void Return(char[] array)
            {
                ArrayPool<char>.Shared.Return(array);
            }
        }
    }
}

可以通过Visual Studio调试> Performance Profiler> .NET对象分配跟踪或Performance Monitor #Gen 0/1集合

来观察分配。

1 个答案:

答案 0 :(得分:2)

部分答案:

  1. 像现在一样设置JsonTextReader.ArrayPool(这也显示在DemoTests.ArrayPooling()中)应该有助于最小化由于在解析过程中分配中间字符数组而造成的内存压力。但是,由于分配了 strings ,这不会减少内存使用,这似乎是您的抱怨。

  2. Release 12.0.1开始,Json.NET可以通过将JsonTextReader.PropertyNameTable设置为某个适当的JsonNameTable子类来重用属性名称字符串的实例。

    JsonSerializer.SetupReader()在反序列化过程中使用此机制在读取器上设置一个名称表,该表返回由contract resolver存储的属性名称,从而防止重复分配预期的已知属性名称序列化器。

    但是,您未使用序列化程序,正在直接阅读,因此没有利用此机制。要启用它,您可以创建自己的自定义JsonNameTable来缓存您实际遇到的属性名称:

    public class AutomaticJsonNameTable : DefaultJsonNameTable
    {
        int nAutoAdded = 0;
        int maxToAutoAdd;
    
        public AutomaticJsonNameTable(int maxToAdd)
        {
            this.maxToAutoAdd = maxToAdd;
        }
    
        public override string Get(char[] key, int start, int length)
        {
            var s = base.Get(key, start, length);
    
            if (s == null && nAutoAdded < maxToAutoAdd)
            {
                s = new string(key, start, length);
                Add(s);
                nAutoAdded++;
            }
    
            return s;
        }
    }
    

    然后按如下所示使用它:

    const int MaxPropertyNamesToCache = 200; // Set through experiment.
    
    var nameTable = new AutomaticJsonNameTable(MaxPropertyNamesToCache);
    
    using (var sr = new MockedStreamReader())
    using (var jtr = new JsonTextReader(sr) { PropertyNameTable = nameTable })
    {
        // Process as before.
    }
    

    这应该大大减少由于属性名称引起的内存压力。

    请注意,AutomaticJsonNameTable将仅自动缓存指定数量的有限名称,以防止发生内存分配攻击。您需要通过实验确定此最大数量。您还可以手动对添加的已知已知属性名称进行硬编码。

    还要注意,通过手动指定名称表,可以防止在反序列化期间使用序列化程序指定的名称表。如果您的解析算法涉及读取文件以查找特定的嵌套对象,然后反序列化这些对象,则可以通过在反序列化之前暂时将名称表置空来获得更好的性能,例如使用以下扩展方法:

    public static class JsonSerializerExtensions
    {
        public static T DeserializeWithDefaultNameTable<T>(this JsonSerializer serializer, JsonReader reader)
        {
            JsonNameTable old = null;
            var textReader = reader as JsonTextReader;
            if (textReader != null)
            {
                old = textReader.PropertyNameTable;
                textReader.PropertyNameTable = null;
            }
            try
            {
                return serializer.Deserialize<T>(reader);
            }
            finally
            {
                if (textReader != null)
                    textReader.PropertyNameTable = old;
            }
        }
    }
    

    需要通过实验来确定使用序列化程序的名称表是否比您自己的性能更好(并且在编写此答案时,我还没有进行任何此类实验)。

  3. 当前,即使跳过或忽略这些值,也无法阻止JsonTextReader为属性值分配字符串。有关类似的增强请求,请参见please should support real skipping (no materialization of properties/etc) #1021

    这里您唯一的选择似乎是派生您自己的JsonTextReader版本并自行添加此功能。您需要查找对SetToken(JsonToken.String, _stringReference.ToString(), ...)的所有调用,并将对__stringReference.ToString()的调用替换为不会无条件分配内存的东西。

    例如,如果您要跳过大量JSON,则可以向string DummyValue添加JsonTextReader

    public partial class MyJsonTextReader : JsonReader, IJsonLineInfo
    {
        public string DummyValue { get; set; }
    

    然后在需要的地方(当前在两个地方)添加以下逻辑:

    string text = DummyValue ?? _stringReference.ToString();
    SetToken(JsonToken.String, text, false);
    

    SetToken(JsonToken.String,  DummyValue ?? _stringReference.ToString(), false); 
    

    然后,当您知道可以略过的读数时,可以将MyJsonTextReader.DummyValue设置为一些存根,例如"dummy value"

    或者,如果您有许多可以预先预测的不可跳过的重复属性值,则可以创建第二个JsonNameTable StringValueNameTable,如果不为空,则尝试在其中查找StringReference像这样:

    var text = StringValueNameTable?.Get(_stringReference.Chars, _stringReference.StartIndex, _stringReference.Length) ?? _stringReference.ToString();
    

    不幸的是,分叉自己的JsonTextReader可能需要大量的持续维护,因为您还需要派生读者使用的所有Newtonsoft实用程序(有很多),并将它们更新为原始版本中的任何重大更改。图书馆。

    您也可以对请求此功能的enhancement request #1021进行投票或评论,或者自己添加类似的请求。