1 of 25

Гигабайт JSON в секунду

Виктор Стародуб, Mail.ru Group

2 of 25

JSON был медленный

  • encoding/json — 250 Мб/с
  • ffjson — 700 Мб/с
  • go/codec — 670 Мб/с
  • "printf" — ненадежно

цифры — для многопоточного режима

3 of 25

Быстрый JSON нужен

  • браузер — только JSON
  • 80-90%% CPU на упаковку — дорого
  • нужно уметь быстро паковать в принципе

4 of 25

ujson vs. ffjson

5 of 25

Сериализация

распаковка

маршалер структуры

lexer

выбор поля

маршалер примитивного типа

упаковка

маршалер структуры

обход полей

маршалер примитивов

буфер

6 of 25

Обход полей структуры: упаковка.

  • reflection 70 Мб/с
  • генерация 600 Мб/с

"dummy" упаковка, тестируем только обход

считаем только данные, не метаданные

7 of 25

Выбор полей структуры: распаковка.

  • reflection (FieldByName) 32 Мб/с
  • reflection (map[string]int) 90 Мб/с
  • генерация (switch fieldName) 190 Мб/с
  • генерация (конечный автомат) 250 Мб/с

8 of 25

Генерация кода: инструменты

  • шаблонизация: gotemplate, text/template, sed, ...
  • go/ast
  • go run + reflection
  • go/types

9 of 25

Упаковка: запись в буфер

  • сразу отдаем по HTTP
  • не обязательно один []byte
  • нужен Content-Length

10 of 25

Упаковка: запись в буфер

  • bytes.Buffer 77 Мб/с
  • b[i] = c 1280 Мб/с
  • make([]byte, 1e6); b[i] = c 830 Мб/с
  • make([]byte, 0, 1e6); b = append(b, c) 380 Мб/с
  • var b []byte; b = append(b, c) 220 Мб/с, 5.9 Мб, 44 аллокации

11 of 25

Упаковка: запись в буфер

append()

лишние копирования

12 of 25

Упаковка: запись в буфер

append()

[][]byte

+

+

13 of 25

Упаковка: запись в буфер

[][]byte

+

+

sync.Pool

14 of 25

Упаковка: запись в буфер

[][]byte

[]byte

+

+

15 of 25

Упаковка: запись в буфер

  • [][]byte 350 Мб/с, ~1.2Мб, 2063 аллокации
  • size *= 2 430 Мб/с, ~1Мб, 79 аллокаций
  • + sync.Pool 550 Мб/с, ~30кБ, 74 аллокации
  • [][]byte -> []byte 350 Мб/с, ~1Мб

16 of 25

Упаковка: запись в буфер

  • маленький пакет + b.RunParallel(): 3400 Мб/с => 180 Мб/с
  • но можно их не делать совсем: 3300 Мб/с (пока не в репозитории)

17 of 25

Упаковка: запись в буфер

  • маленький пакет + b.RunParallel(): 3400 Мб/с => 180 Мб/с
  • но можно их не делать совсем: 3300 Мб/с (пока не в репозитории)
    • начальный буфер (массив) внутри сериализатора
    • и начальный буфер, и сериализатор — на стеке
    • нужно убеждаться, что оно работает именно так (-gcflags "-m", бенчмарки)

18 of 25

Lexer

  • "assertive": l.WantDelim('{')
  • не возвращаем результат через interface{}
  • не дублируем условия лексера

19 of 25

Упаковка/распаковка строк

  • текстовый формат: не знаем длину
  • распаковка работает лучше в два прохода (41б, 83 Мб/с vs 56 Мб/с)
  • копировать кусками быстрее (11б: 880 Мб/с vs 350 Мб/с)
  • ключи вообще не нужно копировать

20 of 25

Вызовы функций

Если функция сериализует по 12 байт:

  • инлайн: 15 Гб/с (бенчмаркаем go test)
  • вызов (обычный или через интерфейс): 3 Гб/с
  • преобразование к интерфейсу + вызов: 2.1 Гб/с

21 of 25

Вызовы функций

  • нужно инлайнить (проверяем "gcflags -m")
  • не делать много не инлайн вызовов в горячем коде
  • интерфейсы не инлайнятся: лексер/буфер не делаем интерфейсом

22 of 25

Unsafe:

  • []byte => string как "переходник" для switch и strconv.ParseUint
  • []byte => string для возврата результатов из метода
  • обход строки по (*uint64) для битовых хаков

23 of 25

ujson vs. easyjson

24 of 25

Результат

  • упаковка: 1600 Мб/с, 169 b/op
  • распаковка: 574 Мб/с, нет лишних аллокаций

25 of 25

Спасибо!

easyjson: github.com/mailru/easyjson

бенчмарки: github.com/vstarodub/parse-microbenchmarks

презентация: goo.gl/rQ6Uxy

Виктор Стародуб, v.starodub@corp.mail.ru