За свои 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 ( |
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 |
Но самой интересной задачей стал потоковый парсинг (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 ( |
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
А покритиковать — в комментариях. Я знаю, вы это любите.
diderevyagin
Есть немало задач, которые требуют flow кодогенерации. К примеру grpc. или генерация кода по openapi спеке. и прочее и прочее. в том числе работа с json
мне кажется лучший выход - автоматизировать и наслаждаться.
А путь unsafe ... "Если в первом акте на стене висит ружье, то в последнем оно должно выстрелить". Впрочем сама статья и сравнения и решение хороши