Сравнительный анализ десериализации Newtonsoft.Json: из потока и из строки - PullRequest
3 голосов
/ 05 июня 2019

Меня интересует сравнение производительности (скорости, использования памяти) двух подходов к десериализации полезной нагрузки JSON ответа HTTP с использованием Newtonsoft.Json .

Мне известно о * 1005Советы по повышению производительности * Newtonsoft.Json для использования потоков, но я хотел знать больше и иметь точные цифры.Я написал простой тест, используя BenchmarkDotNet , но я немного озадачен результатами (см. Цифры ниже).

Что я получил:

  • парсинг отпоток всегда быстрее, но на самом деле не очень
  • разбирает маленький и "средний" JSON имеет лучшее или равное использование памяти при использовании строки в качестве ввода
  • значительная разница в использовании памяти начинает замечаться при большихJSON (где сама строка заканчивается в LOH)

У меня не было времени, чтобы выполнить правильное профилирование (пока), я немного удивлен перегрузкой памяти при потоковом подходе (если нет ошибки),Весь код здесь .

?

  • Правильный ли мой подход?(использование 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 МБ
  • Строка: среднее значение31,22 мс, 36,01 мб выделено

средний JSON

повторяется 1000 раз

  • Поток: в среднем 24,07 мс, 12 мб выделено
  • Строка: среднее значение 25,09 мс, выделено 12,85 МБ

Большой JSON

Повторяется 100 раз

  • Stream: среднее значение 229,6 мс, 47,54 МБ выделено, объекты получены для Gen 1
  • Строка: среднее значение 240,8 мс, 92,42 МБ выделено, объекты получены для Gen 2!

Обновление

Я прошел через источник JsonConvert и обнаружил, что он внутренне использует JsonTextReader с StringReader при десериализации из string: JsonConvert: 816 .Там также задействован поток (конечно!).

Тогда я решил больше копаться в StreamReader, и с первого взгляда был потрясен - он всегда выделяет буфер массива (byte[]): StreamReader: 244 , что объясняет использование памяти.

Это дает мне ответ на вопрос «почему».Решение простое - используйте меньший размер буфера при создании экземпляра StreamReader - минимальный размер буфера по умолчанию равен 128 (см. StreamReader.MinBufferSize), но вы можете указать любое значение > 0 (проверьте одно из значений перегрузки ctor).

Конечно размер буфера оказывает влияние на обработку данных.Отвечая на вопрос, какой размер буфера мне следует использовать: это зависит .Ожидая меньших ответов JSON, я думаю, что безопасно придерживаться небольшого буфера.

1 Ответ

2 голосов
/ 08 июня 2019

После некоторой путаницы я обнаружил причину выделения памяти при использовании StreamReader.Исходное сообщение обновляется, но повторяем здесь:

StreamReader использует значение по умолчанию bufferSize, установленное на 1024. Каждое создание экземпляра StreamReader затем выделяет байтовый массив этого размера.Вот почему я видел такие цифры в своем тесте.

Когда я установил bufferSize на самое низкое из возможных значений 128, результаты оказались намного лучше.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...