Newtonsoft json.net JsonTextReader Сборщик мусора интенсивно - PullRequest
1 голос
/ 23 апреля 2019

Мы потребляем большой (ГБ) сетевой поток, сериализованный как JSON, через http, используя пакет nuget Newtonsoft.Json для десериализации потока ответа в записи в памяти для дальнейшей манипуляции.

Учитывая чрезмерные объемы данных, мы используем потоковую передачу для получения порции ответа за раз и хотели бы оптимизировать этот процесс, поскольку мы достигаем ограничений ЦП.

Один из кандидатов на оптимизацию - это JsonTextReader , который постоянно выделяет новые объекты и, следовательно, запускает сборку мусора.

Мы следовали советам Newtonsoft Советы по повышению производительности .

Я создал пример консольного приложения .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> Профилировщик производительности> Отслеживание распределения объектов .NET или Монитор производительности #Gen 0/1 Коллекции

1 Ответ

2 голосов
/ 25 апреля 2019

Отвечая по частям:

  1. Настройка JsonTextReader.ArrayPool, как вы уже делаете (что также показано в DemoTests.ArrayPooling()), должна помочь минимизировать нагрузку на память из-за выделения промежуточного звена массивы символов во время синтаксического анализа. Это, однако, не уменьшит использование памяти из-за выделения строк , что кажется вашей жалобой.

  2. Начиная с Выпуск 12.0.1 , Json.NET имеет возможность повторно использовать экземпляры строк с именами свойств , установив JsonTextReader.PropertyNameTable в некоторые соответствующие JsonNameTable подкласс.

    Этот механизм используется во время десериализации, JsonSerializer.SetupReader(), чтобы установить таблицу имен в считывателе, которая возвращает имена свойств, хранящиеся в распознавателе контракта , таким образом предотвращая повторное выделение известных имен свойств, ожидаемых сериализатором.

    Однако вы не используете сериализатор, вы читаете напрямую и поэтому не пользуетесь этим механизмом. Чтобы включить его, вы можете создать свой собственный 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 выделять строки для значений свойств, даже если пропустить или иным образом игнорировать эти значения. См. , пожалуйста, поддержите реальный пропуск (без материализации свойств и т. Д.) # 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, используемые читателем (их много), и обновить их до любых критических изменений в исходной библиотеке.

    Вы также можете проголосовать или прокомментировать запрос на улучшение # 1021 , запрашивающий эту способность, или добавить аналогичный запрос самостоятельно.

...