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

Newtonsoft json.net JsonTextReader垃圾收集器密集型,.net,performance,garbage-collection,json.net,.net,Performance,Garbage Collection,Json.net,我们正在使用一个通过http序列化为JSON的大型(GBs)网络流,使用Newtonsoft.JSON nuget包将响应流反序列化到内存记录中,以便进一步操作 鉴于数据量过大,我们使用流式处理一次接收一大块响应,并希望在达到CPU限制时优化此过程 优化的候选对象之一似乎是JsonTextReader,它不断地分配新对象,从而触发垃圾收集 我们遵循了Newtonsoft的建议 我创建了一个示例.net控制台应用程序,模拟当JsonTextReader读取响应流时分配新对象的行为,分配表示属性名称

我们正在使用一个通过http序列化为JSON的大型(GBs)网络流,使用Newtonsoft.JSON nuget包将响应流反序列化到内存记录中,以便进一步操作

鉴于数据量过大,我们使用流式处理一次接收一大块响应,并希望在达到CPU限制时优化此过程

优化的候选对象之一似乎是JsonTextReader,它不断地分配新对象,从而触发垃圾收集

我们遵循了Newtonsoft的建议

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

问题: 如果在现实世界中有95%的属性名/值实例是重复的(在测试中是相同的记录,所以100%重复),那么我们还可以调整/重写哪些内容来重用已经分配的属性名/值实例

示例应用程序:

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

使用系统;
使用系统缓冲区;
使用System.IO;
使用System.Linq;
使用系统文本;
使用Newtonsoft.Json;
名称空间JSONNETSTER
{
班级计划
{
静态void Main(字符串[]参数)
{
使用(var sr=new MockedStreamReader())
使用(var jtr=新的JsonTextReader(sr))
{
//似乎没有什么区别
//jtr.ArrayPool=JsonArrayPool.Instance;
//每次读取都会分配新对象
while(jtr.Read())
{
}
}
}
//模拟序列化为json的连续记录流
公共类MockedStreamReader:StreamReader
{
private bool initialProvided=false;
私有字节[]initialBytes=Encoding.Default.GetBytes(“[”);
私有静态只读字节[]记录字节;
int nextStart=0;
静态MockedStreamReader()
{
var recordSb=新的StringBuilder(“{”);
//生成{“键[i]:“值[i]”的[i],
Enumerable.Range(0,50).ToList().ForEach(i=>
{
如果(i>0)
{
记录b.附加(“,”);
}
recordSb.Append($“\”键{i}\:\”值{i}\”);
});
recordSb.Append(“},”);
recordBytes=Encoding.Default.GetBytes(recordSb.ToString());
}
public-MockedStreamReader():基(新内存流())
{   }
公共重写整型读取(字符[]缓冲区,整型索引,整型计数)
{
//继续读取循环中的相同记录
如果(提供此首字母缩写)
{
var start=nextStart;
var length=Math.Min(recordBytes.length-start,count);
var end=开始+长度;
nextStart=end>=recordBytes。长度?0:end;
复制(记录字节、开始、缓冲区、索引、长度);
返回长度;
}
其他的
{
initialProvided=true;
复制(initialBytes、缓冲区、initialBytes.Length);
返回initialBytes.Length;
}
}
}
//尝试在序列化中重用数据
公共类JsonArrayPool:IArrayPool
{
公共静态只读JsonArrayPool实例=新JsonArrayPool();
公共字符[]租金(整数最小长度)
{
返回ArrayPool.Shared.Rent(最小长度);
}
公共void返回(char[]数组)
{
ArrayPool.Shared.Return(数组);
}
}
}
}
可以通过Visual Studio Debug>Performance Profiler>.NET对象分配跟踪或Performance Monitor#Gen 0/1集合来观察分配情况

分部分回答:

  • 您正在进行的设置(也如中所示)应有助于将解析过程中由于分配中间字符数组而导致的内存压力降至最低。但是,它不会因为分配字符串而减少内存使用,这似乎是您的抱怨

  • 到目前为止,Json.NET能够通过设置适当的子类来重用属性名字符串的实例

    此机制在反序列化期间由用于在读取器上设置名称表,该表返回由存储的属性名称,从而防止重复分配序列化程序预期的已知属性名称

    但是,您没有使用序列化程序,而是直接读取,因此没有利用此机制。要启用此机制,您可以创建自己的自定义
    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;
        }
    }
    
    这将大大减少由于属性名称而产生的内存压力

    请注意,
    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;
            }
        }
    }
    
    然后在需要的地方添加以下逻辑(在两个p中
    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.
    }
    
    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;
            }
        }
    }
    
    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); 
    
    var text = StringValueNameTable?.Get(_stringReference.Chars, _stringReference.StartIndex, _stringReference.Length) ?? _stringReference.ToString();