За свои 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
А покритиковать — в комментариях. Я знаю, вы это любите.
Комментарии (20)

Valsha
29.06.2026 15:17В версии 1.26 появился json2
С ним не сравнивали?

ihar76 Автор
29.06.2026 15:17насколько я помню, это экспериментальная версия, по крайней мере в 1.26.3
поэтому в репозитории она не присутствует. Но специально для ответа я провел тесты и получил=== SilentJSON vs encoding/json v1 vs encoding/json/v2 (Go 1.26.3) ===
Platform: AMD Ryzen 9 7950×3D, Windows, GOEXPERIMENT=jsonv2
— Unmarshal 3M records (482 MB JSON, sequential) —
SilentJSON-32 1 753595900 ns/op 671.42 MB/s 65368888 B/op 6 allocs/opStdJSON_v2-32 1 2698572700 ns/op 187.50 MB/s 1985055064 B/op 18910718 allocs/opStdJSON_v1-32 1 3108066300 ns/op 162.80 MB/s 1985062216 B/op 18910832 allocs/op— Unmarshal 100K records (parallel) —
SilentJSON_Parallel-32 3 4823467 ns/op 3293.48 MB/s 6021384 B/op 163 allocs/opStdJSON_v1-32 3 80785100 ns/op 196.65 MB/s 2163085 B/op 220401 allocs/opStdJSON_v2-32 3 82894933 ns/op 191.64 MB/s 19606984 B/op 820401 allocs/op— Marshal 100K records —
SilentJSON-32 3 11557700 ns/op 1409.30 MB/s 0 B/op 0 allocs/opStdJSON_v2-32 3 40116033 ns/op 396.00 MB/s 15892528 B/op 3 allocs/opStdJSON_v1-32 3 41471367 ns/op 383.06 MB/s 15892528 B/op 3 allocs/op— Stream Decode 3M records (482 MB JSON) —
SilentJSON_Stream-32 1 836256500 ns/op 605.05 MB/s 41405248 B/op 7714289 allocs/opStdJSON_v2_Stream-32 1 2917844500 ns/op 173.41 MB/s 1985114304 B/op 18910791 allocs/opStdJSON_v1_Stream-32 1 3703443800 ns/op 136.62 MB/s 106551440 B/op 5118604 allocs/op

AcckiyGerman
29.06.2026 15:17Вы проделали очень крутую работу! Настоящий Highload!
К сожалению, ускорение библиотек на низком уровне часто нивелируется неоптимальной архитектурой на высоком. То самое “новая версия ОС ускорила открытие программ на 20%, тем временем программы потолстели на 200%”.
каждый час парсить сотню прайсов от поставщиков (до 6 млн позиций на файл) для крупного портала
С архитектурой портала что-то явно пошло не туда. Уверен что 99.9% позиций в этих прайсах не меняются так часто. Было бы правильнее передавать дельту изменений через API, а не тянуть по сети гигабайтные Json’ы сотнями каждый час. Вероятно после парсинга эти прайсы ещё и базу данных попусту нагружают.

diderevyagin
29.06.2026 15:17Разные случаи бывают. много лет назад я работал в команде, там клиента мобильного писали индусы, back и web ui - мы. нормальный flow отправки обновлений с клиента так и не наладили - слали весь state как есть. там были не сотни мегабайт, но до пары мегабайт на порцию было ... много чего было.
И этот цирк длился до тех пор, пока не передали другой команде (нам).

ihar76 Автор
29.06.2026 15:17Очень хорошее замечание, но проблема не в анализе содержимого, а просто в том, что бы его получить. А так да, изменения тоже проверялись. На передачу данных мы никак повлиять не могли, увы.

Blacpaul57
29.06.2026 15:17Добро пожаловать в суровый энтерпрайз. Поставщики шлют то что умеют, а ты должен это как-то переваривать

900k
29.06.2026 15:17Не смог проверить правду вы говорите или нет.
Ваша библиотека работает только на amd64.

mosinnik
29.06.2026 15:17а что насчет json с малым объемом, по типу 5-10 объектов? что там будет по аллокациям и скорости по сравнению с остальными

ihar76 Автор
29.06.2026 15:17можно протестировать, но у меня графики линейные, и большого разлета не будет. Может просесть на процентов 30 в пике (если многозадачность не сработает). Но все равно это гораздо больше стандартных библиотек.

mosinnik
29.06.2026 15:17тут момент, что обычно алгоритмы если на большом объеме дают хороший выигрыш, то могут заметно проигрывать в мелких задачах на частых вызовах. Вот и хочется понять что там будет, когда надо лишь один жиденький реквест с json распарсить, но сделать это надо условно 100к раз в секунду на всех ядрах и тот же AVX2 может начать замедлять

ihar76 Автор
29.06.2026 15:17справедливо, я добавил сравнение, не полное, но смысл понятен в описании в гите подправил график и табличку красивее. файлики по 5 записей.
LibraryThroughput (MB/s)Latency (ns/op)Memory Allocated Allocs/opSilentJSON (Parallel) 735.98MB/s 877.7ns 25B 1
Sonic 317.21MB/s 2036ns 2632B 21
Standard (
encoding/json) 84.61MB/s 7635ns 2528B 52

Blacpaul57
29.06.2026 15:17Если вы гоняете гигабайты json по сети, то проблема скорее всего в выборе формата, а не в парсере. Protobuf придумали не просто так

ihar76 Автор
29.06.2026 15:17Совершенно верно. Но мы имеем то что имеем. Форматы не всегда мы выбираем. Это хорошо когда продукт не зависит от внешнего бизнеса. А внешнему бизнесу важнее другие вещи.

AngryEvilCookie
29.06.2026 15:17Зачем сравнение с бинарным протобаф? Я к тому что гигайбайты json в бинарном формате превратятся в мегабайты, возможно килобайты. Как при этом сравнивалась пропускная способность не совсем понятно. Все прочитал, но все равно слабо верится что бинарный протокол проигрывает текстовому.
diderevyagin
Есть немало задач, которые требуют flow кодогенерации. К примеру grpc. или генерация кода по openapi спеке. и прочее и прочее. в том числе работа с json
мне кажется лучший выход - автоматизировать и наслаждаться.
А путь unsafe ... "Если в первом акте на стене висит ружье, то в последнем оно должно выстрелить". Впрочем сама статья и сравнения и решение хороши
ihar76 Автор
Вы абсолютно правы, тут всегда вопрос выбора инструмента под задачу.
Я пришел к этому решению через боль: когда нужно было каждый час парсить сотню прайсов от поставщиков (до 6 млн позиций на файл) для крупного портала, вопрос скорости парсинга стал критическим. Было обидно осознавать, что «бутылочное горлышко» — это не сложная бизнес-логика или нехватка мощностей, а именно сам парсер, который просто не успевал переваривать валидные данные. Причем ситуация усугублялась тем, что форматы у поставщиков были абсолютно разные и постоянно менялись.
Если бы такой инструмент был под рукой тогда, я бы сэкономил массу времени и нервов. В общем, как вы и сказали — «ружье» выстрелило именно там, где это было нужно для выживания проекта
Blacpaul57
Кодогенерация хороша, пока у тебя не появится циклическая зависимость в спеке и пайплайн не упадет с невнятной ошибкой