JSON на 40 ГБ/с: почему zstd обгоняет gzip в 16 раз

Даниэль Лемир разобрал 812 МБ сжатого NDJSON с помощью simdjson: gzip упирается в 2,5 ГБ/с, а zstd с независимыми фреймами даёт 40 ГБ/с на 64 потоках при почти том же размере файла.

Даниэль Лемир, автор библиотеки simdjson, проверил простую вещь: насколько быстро можно прочитать большой сжатый файл с JSON. Результат получился показательным. Один и тот же набор данных разбирается со скоростью 2,5 ГБ/с из gzip и 40 ГБ/с из zstd, а размер файлов почти не отличается.

Что за данные

Речь о формате NDJSON, он же JSON Lines: по одному JSON-документу на строку. Так часто выглядят логи, выгрузки из баз данных и датасеты. Файлы бывают огромными, поэтому их сжимают.

{"id":1,"active":true,"user":{"name":"user_1","tags":["guest"]},"score":-3862,"note":"..."}
{"id":2,"active":false,"user":{"name":"user_2","tags":["staff","admin"]},"score":8123,"note":"..."}

Тестовый файл содержит 5 млн записей, это 812 МБ NDJSON. Для каждой записи программа читает несколько полей: считает активные записи и суммирует score у активных пользователей с тегом admin.

Как разбирать сжатый поток по кускам

Распаковывать весь файл на диск необязательно. Можно распаковать кусок, разобрать в нём все целые строки, а незаконченную последнюю строку перенести в начало буфера. В NDJSON перевод строки не может встретиться внутри документа, потому что внутри строковых значений он экранируется. Значит, буфер всегда можно резать сразу после последнего символа новой строки.

Для множества документов в одном буфере в simdjson есть функция iterate_many:

simdjson::ondemand::parser parser;
while (true) {
  // заполняем буфер распакованными байтами...
  size_t cut = eof ? len : last_newline(buf, len) + 1;
  simdjson::ondemand::document_stream stream;
  parser.iterate_many(buf, cut, cut).get(stream);
  for (auto doc : stream) {
    accumulate(doc.value_unsafe(), result);
  }
  if (eof) { break; }
  std::memmove(buf, buf + cut, len - cut);
  len -= cut;
}

Тесты шли на сервере с двумя Intel Xeon Gold 6548N (Emerald Rapids): 64 ядра, 128 потоков, компилятор GCC 14.

Почему gzip упирается в один поток

Файл gzip представляет собой один длинный сжатый поток. Чтобы декодировать следующий фрагмент, распаковщику нужны предыдущие 32 КиБ вывода. Поэтому распаковка идёт строго с начала файла и в одном потоке.

Разбор при этом можно распараллелить: один поток распаковывает куски и кладёт их в очередь, остальные парсят. Так скорость удваивается и доходит до 2,5 ГБ/с. Дальше gzip не пускает.

Как zstd и lz4 дают 40 ГБ/с

Файлы zstd и lz4 могут состоять из множества независимых фреймов, идущих друг за другом. Это по-прежнему обычный .zst или .lz4, стандартные утилиты распаковывают его как всегда.

Программа Лемира начинает новый фрейм каждые 256 КиБ JSON, всегда сразу после перевода строки. Каждый фрейм хранит свой распакованный размер и контрольную сумму. Чтобы найти конец фрейма, достаточно прочитать несколько заголовков блоков, ничего не распаковывая. В результате потоки берут фреймы по одному, и каждый сам распаковывает и разбирает свою порцию.

В одном потоке zstd и lz4 не быстрее gzip, около 1,1 ГБ/с. Зато на 64 потоках zstd выдаёт 40 ГБ/с, а lz4 34 ГБ/с. Это в 16 раз быстрее лучшего результата с gzip.

Скорость разбора NDJSON для gzip, zstd и lz4

Сколько места это стоит

Маленькие фреймы сжимаются немного хуже, ведь каждый начинается с нуля. Но разница небольшая: файл zstd всего на 6% больше, чем gzip.

ФайлРазмер
NDJSON без сжатия812,0 МБ
gzip (один поток)56,9 МБ
zstd (фреймы по 256 КиБ)60,6 МБ
lz4 (фреймы по 256 КиБ)108,5 МБ

Лемир оговаривается, что данные синтетические и очень однообразные, поэтому распаковка идёт быстро. На реальных данных цифры будут скромнее.

Что с этим делать

Если вы сами пишете файлы JSON Lines, сжимайте их в zstd с множеством фреймов. Размер будет почти как у gzip, а читать такие файлы на многоядерной машине можно в разы быстрее. Для логов, выгрузок и датасетов, которые читают чаще, чем пишут, это почти бесплатное ускорение.

Исходный код демо: github.com/simdjson/simdjson_compressed_demo.

Источник: статья Даниэля Лемира в X.

Поделиться:

Ответить

Ваш адрес email не будет опубликован. Обязательные поля помечены *