За свои 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

Но самой интересной задачей стал потоковый парсинг (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

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

Комментарии (20)


  1. diderevyagin
    29.06.2026 15:17

    Дополнительные шаги сборки, забытые команды go:generate, конфликты в пайплайнах.

    Есть немало задач, которые требуют flow кодогенерации. К примеру grpc. или генерация кода по openapi спеке. и прочее и прочее. в том числе работа с json

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

    А путь unsafe ... "Если в первом акте на стене висит ружье, то в последнем оно должно выстрелить". Впрочем сама статья и сравнения и решение хороши


    1. ihar76 Автор
      29.06.2026 15:17

      Вы абсолютно правы, тут всегда вопрос выбора инструмента под задачу.

      Я пришел к этому решению через боль: когда нужно было каждый час парсить сотню прайсов от поставщиков (до 6 млн позиций на файл) для крупного портала, вопрос скорости парсинга стал критическим. Было обидно осознавать, что «бутылочное горлышко» — это не сложная бизнес-логика или нехватка мощностей, а именно сам парсер, который просто не успевал переваривать валидные данные. Причем ситуация усугублялась тем, что форматы у поставщиков были абсолютно разные и постоянно менялись.

      Если бы такой инструмент был под рукой тогда, я бы сэкономил массу времени и нервов. В общем, как вы и сказали — «ружье» выстрелило именно там, где это было нужно для выживания проекта


    1. Blacpaul57
      29.06.2026 15:17

      Кодогенерация хороша, пока у тебя не появится циклическая зависимость в спеке и пайплайн не упадет с невнятной ошибкой


  1. Valsha
    29.06.2026 15:17

    В версии 1.26 появился json2

    С ним не сравнивали?


    1. 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/op

      StdJSON_v2-32 1 2698572700 ns/op 187.50 MB/s 1985055064 B/op 18910718 allocs/op

      StdJSON_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/op

      StdJSON_v1-32 3 80785100 ns/op 196.65 MB/s 2163085 B/op 220401 allocs/op

      StdJSON_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/op

      StdJSON_v2-32 3 40116033 ns/op 396.00 MB/s 15892528 B/op 3 allocs/op

      StdJSON_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/op

      StdJSON_v2_Stream-32 1 2917844500 ns/op 173.41 MB/s 1985114304 B/op 18910791 allocs/op

      StdJSON_v1_Stream-32 1 3703443800 ns/op 136.62 MB/s 106551440 B/op 5118604 allocs/op


  1. AcckiyGerman
    29.06.2026 15:17

    Вы проделали очень крутую работу! Настоящий Highload!

    К сожалению, ускорение библиотек на низком уровне часто нивелируется неоптимальной архитектурой на высоком. То самое “новая версия ОС ускорила открытие программ на 20%, тем временем программы потолстели на 200%”.

    каждый час парсить сотню прайсов от поставщиков (до 6 млн позиций на файл) для крупного портала

    С архитектурой портала что-то явно пошло не туда. Уверен что 99.9% позиций в этих прайсах не меняются так часто. Было бы правильнее передавать дельту изменений через API, а не тянуть по сети гигабайтные Json’ы сотнями каждый час. Вероятно после парсинга эти прайсы ещё и базу данных попусту нагружают.


    1. diderevyagin
      29.06.2026 15:17

      Разные случаи бывают. много лет назад я работал в команде, там клиента мобильного писали индусы, back и web ui - мы. нормальный flow отправки обновлений с клиента так и не наладили - слали весь state как есть. там были не сотни мегабайт, но до пары мегабайт на порцию было ... много чего было.

      И этот цирк длился до тех пор, пока не передали другой команде (нам).


    1. ihar76 Автор
      29.06.2026 15:17

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


    1. Blacpaul57
      29.06.2026 15:17

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


  1. 900k
    29.06.2026 15:17

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


    1. ihar76 Автор
      29.06.2026 15:17

      я могу дополнить для вашей платформы поддержку, если напишите какую. Более того, буду благодарен, если поможете протестировать в своих условиях


      1. 900k
        29.06.2026 15:17

        arm (apple silicon)


        1. ihar76 Автор
          29.06.2026 15:17

          UPS немного отладки еще нужно. скоро выложу


  1. mosinnik
    29.06.2026 15:17

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


    1. ihar76 Автор
      29.06.2026 15:17

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


      1. mosinnik
        29.06.2026 15:17

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


        1. ihar76 Автор
          29.06.2026 15:17

          справедливо, я добавил сравнение, не полное, но смысл понятен в описании в гите подправил график и табличку красивее. файлики по 5 записей.

          LibraryThroughput (MB/s)Latency (ns/op)Memory Allocated Allocs/op

          SilentJSON (Parallel) 735.98MB/s 877.7ns 25B 1

          Sonic 317.21MB/s 2036ns 2632B 21

          Standard (encoding/json) 84.61MB/s 7635ns 2528B 52


  1. Blacpaul57
    29.06.2026 15:17

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


    1. ihar76 Автор
      29.06.2026 15:17

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


  1. AngryEvilCookie
    29.06.2026 15:17

    Зачем сравнение с бинарным протобаф? Я к тому что гигайбайты json в бинарном формате превратятся в мегабайты, возможно килобайты. Как при этом сравнивалась пропускная способность не совсем понятно. Все прочитал, но все равно слабо верится что бинарный протокол проигрывает текстовому.