За свои 17+ лет в активной разработке я встречал много проблем, но одна преследовала меня постоянно: JSON. Нет, с самим форматом все ок, но вот с его чтением — не все норм.

Когда я только начинал работать с PHP, я списывал это на скриптовость языка. Отчасти из‑за этого я даже поменял стек. Но когда приходили по‑настоящему большие файлы, это всегда было больно. Иногда — очень. Был проект, где мы ждали не обработку информации бизнес‑логикой, а банального парсинга. Файлы доходили до десятков гигабайт и не всегда влезали в оперативку. Тогда я и заработал себе персональный todo — разобраться с этим раз и навсегда.

Сейчас, находясь в поиске новых возможностей, я решил вспомнить эту старую боль. Я уже давно не PHP‑разработчик, но проблема в индустрии всё та же. Объемы данных растут, требования тоже, а воз и ныне там. Нет, есть море крутых решений. Даже тут, на Хабре. Но для меня всё не то.

Мне нужно решение, а не костыль. То есть: никакой кодогенерации и никаких JIT (я не противник JIT, просто не хочу тянуть эту сложность).

Я ступил на тонкий лед: в Go есть классная штука — пакет unsafe. Почему классная? Потому что она позволяет обойти тяжелые ненужные проверки. Плюс побитовые операции для ускорения всего, до чего только смогли дотянуться руки. Пока изучал чужие парсеры, столкнулся с обманом в репозиториях, подкручиванием статистики (куда же без него?) и перекладыванием ответственности (и аллокаций) на сторону разработчиков.

Часть 1. Путь разочарований, или почему меня не устроили лидеры рынка

Когда стандартный encoding/json перестает справляться, люди обычно идут по одному из трех путей:

  • Кодогенерация (easyjson и аналоги). Скорость растет, но Developer Experience падает ниже нуля. Дополнительные шаги сборки, забытые команды go:generate, конфликты в пайплайнах. Я хотел инструмент, который работает «из коробки» как стандартная библиотека, а не усложняет процесс разработки.

  • JIT‑компиляция (Sonic). Выглядит потрясающе на бенчмарках, но имеет скрытую цену — «холодный старт». Каждый раз, когда парсер встречает новую структуру, он тратит время на компиляцию машинного кода в рантайме (скорость падает до ~800 MB/s). Пиковая скорость крутая, честно. Но цена — нестабильность задержек на рандомных данных, отсутствие чтения из потока и отсутствие генерации JSON.

  • C++ порты и SIMD (simdjson‑go). Невероятно быстро, но API основан на AST (Abstract Syntax Tree). Чтобы замапить данные в обычные Go‑структуры, разработчику приходится писать кучу ручного, низкоуровневого кода. Я прифигел и плюнул, когда увидел это безобразие. По сути, непосредственное конвертирование типов просто не учитывается в их бенчмарках. Это скрытие информации.

Часть 2. Идея: Zero‑Allocation, Zero‑Warmup и никакого ручного парсинга

Я понял, что нужен инструмент, который объединит удобство encoding/json и скорость C++ портов.

Многие статьи на Хабре, рассказывающие о «сверхбыстром парсинге», сводятся к одному трюку: авторы заставляют программиста вручную писать методы Decode для каждой структуры, жестко привязываясь к порядку полей. Если API на клиенте поменяет местами TraceID и Timestamp, такой парсер молча сломает данные.

Я пошел другим путем. silentjson использует Precomputed Registry. Библиотека использует reflect ровно один раз — на этапе старта приложения. Она строит внутреннюю карту структуры, а затем работает с ней без оглядки на то, в каком порядке прилетят ключи в JSON. Никакого JIT‑прогрева — максимальная пропускная способность с первого же запроса.

Часть 3. Технический хардкор и парадокс потокового чтения

Чтобы добиться скорости, я реализовал AVX2 Tape‑Scanner — сканер на битовых масках и SIMD‑инструкциях, который размечает JSON без скалярных циклов. А парсинг строк работает через unsafe.String (Zero‑Copy), ссылаясь прямо на исходный буфер.

Library

Throughput (MB/s)

Latency (ns/op)

Memory Allocated

Allocs/op

SilentJSON

1454.91 MB/s 👑

10,222,408 ns 👑

0 MB (Zero‑Alloc) 👑

0 👑

Sonic

1400.53 MB/s

11,342,853 ns

78.18 MB

37

Standard (encoding/json)

596.53 MB/s

26,630,475 ns

15.15 MB

2

Protobuf

452.45 MB/s

15,042,191 ns

6.49 MB

1

Нужно отметить, что protobuf тут не заслуженно стоит, ведь он имеет теоретически компактнее структуру. поэтому тут вернее сравнить сколько раз за 1 секуну он обработал наш файл.

op/s

ns/op

MB/s

B/op

Allocs/op

SilentJSON

241

5076327

3129.43

82334

132

Protobuf

36

32726719

207.96

39120496

1100019

Как видим, это не изменило его позицию относительно SilentJSON, даже усугубило

Но самой интересной задачей стал потоковый парсинг (io.Reader).

Парадокс стриминга в мире Go заключается в том, что большинство библиотек (например, Jsoniter), заявляющих поддержку Stream, на самом деле буферизируют гигантские куски данных в памяти. Они ждут закрывающей скобки массива, накапливая состояния и создавая дикое давление на Garbage Collector (до 14.6M аллокаций в тестах).

В silentjson я сделал честный StreamDecoder.

  • NextRaw(): Позволяет «на лету» вырывать сырые JSON‑объекты из потока на скорости ~1.2 GB/s.

  • NextChan(): Асинхронный Producer‑Consumer режим, который под капотом использует Ring Buffer. Это дает возможность парсить данные в фоновой горутине без data races и с нулевыми дополнительными аллокациями, передавая объекты в основной поток. Таким образом, несмотря на чуть меньшую пиковую скорость в бенчмарке, в реальных приложениях это работает быстрее за счет отсутствия пауз и блокировок бизнес‑логики.

Сколько времени и сил ушло на постоянную отладку — не пересказать. Причем изначально я написал сканер на чистом Go. В тепличных микробенчмарках он даже показывал скорость чуть выше и давал меньше аллокаций. Но ассемблер дал главное — предсказуемое чтение данных и плоский, линейный график на выходе. В production предсказуемость задержки (tail latency) всегда дороже пиковой скорости.

На потоковых данных я вообще оторвался. Захотел сделать фишки, которые реально помогают в проектах. Пусть они не такие изящные внутри, как обычный Unmarshal, но это одни из самых быстрых вариантов на рынке, которые могут поспорить с решениями на C или Rust.

Ну и отдельное удовольствие — это сравнение с gRPC. По сути, бинарные форматы сейчас часто выступают не только как «тормоз» из‑за оверхеда на десериализацию структур, но и приносят постоянные траблы с версионностью и синхронизацией контрактов протокола.

Library

Throughput (MB/s)

Memory Allocated

Allocs/op

Notes

SilentJSON (NextRaw)

~1181 MB/s 🚀

526 MB

3.0M

Extreme speed raw stream chunk extraction

SilentJSON (Decode)

469.96 MB/s 👑

41 MB 👑

7.7M 👑

Full Go Struct Binding, zero alloc iteration

Jsoniter (Stream)

455.51 MB/s

148 MB

14.6M

2x more GC pressure

SilentJSON (NextChan)

378.02 MB/s ⚡

41 MB 👑

7.7M 👑

Async Producer‑Consumer mode (Ring Buffer)

Standard (json.NewDecoder)

105.42 MB/s

162 MB

13.3M

Slowest, highest memory usage

Часть 4. Бенчмарки: плоская линия как признак качества

Я тестировал парсер на массивах из 100 000 сложных вложенных объектов (~18MB). Причем поля в объектах специально менялись местами, чтобы исключить читерство с порядком. Результаты:

Объем

10k объектов

25k объектов

50k объектов

100k объектов

SilentJSON

3050

3183

3320

3347

Sonic

421

459

463

467

encoding/json

106

106

107

107

  • Десериализация (Parallel): 3347 MB/s против 107 MB/s у encoding/json.

  • Аллокации: 4 allocs/op у нас против 10 002 у Sonic и 509 997 у стандарта.

  • Сериализация: 1454 MB/s (Zero‑Alloc).

Но моя главная гордость — это графики масштабирования. В отличие от других библиотек, которые деградируют при росте объема данных из‑за промахов кэша или работы GC, график производительности silentjson — это прямая горизонтальная линия. Это доказывает, что сложность нашего парсера строго O(N), и он абсолютно предсказуем под любой нагрузкой.

Вывод: unsafe — это не ругательство

Да, библиотека активно использует пакет unsafe. Да, Zero‑Copy означает, что вы не можете изменять исходный байтовый срез, пока работаете со строками из него.

Но в мире высоконагруженного бекенда производительность требует дисциплины. Если ваша система задыхается от объемов JSON, а покупка новых серверов больше не решает проблему — иногда нужно просто перестать генерировать мусор.

Проект полностью открыт, работает на Go 1.18+ (Generics) и готов к использованию.

Код можно посмотреть тут: https://github.com/GenshIv/silentjson

А покритиковать — в комментариях. Я знаю, вы это любите.