Files
clickstream-data-platform/docs/research/2026-08-01-python-batch-generation-speed.md
T
ddadminandClaude Opus 5 f70087c438 docs(research): скорость батчевой генерации в Python — порядки величин
Зачем: развилке производительности генератора (#30) нужны числа вместо
догадок — тикет #31 просил ответ по первоисточникам.

Что: заметка docs/research/2026-08-01-python-batch-generation-speed.md —
бенчмарки авторов orjson (5–14x к stdlib json), таблица производительности
генераторов numpy (~3 нс на значение), доки multiprocessing (обход GIL),
бенчмарк mimesis против Faker (~24x), кросс-проверка msgspec; плюс локальная
проверка порядков через uv.

Проверка: ссылки на первоисточники в тексте; локальный микробенчмарк
воспроизводится `uv run --with numpy --with orjson`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 15:54:28 +03:00

160 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Скорость батчевой генерации событийных данных в Python: порядки величин
Дата: 2026-08-01. Тикет: #31 (часть карты #26). Потребитель: развилка
производительности генератора (#30, спека v2, разделы 9 и 11).
## Вопрос
Какой скорости (строк или событий в секунду) реально ждать от батчевой
генерации событий в Python и что для неё берут: numpy/векторная генерация
против посточной, orjson против stdlib json, multiprocessing по дням?
Ответ — по первоисточникам (документация и бенчмарки авторов библиотек).
## Краткий вывод
Порядки величин на одно ядро CPU:
| Подход | Порядок скорости | Опора |
|---|---|---|
| Случайные значения numpy (векторно) | ~10^8 значений/с | таблица numpy |
| Векторная сборка колонок события | ~10^6–10^7 строк/с | локальная проверка |
| Посточная генерация (random + dict) | ~10^410^5 строк/с | локальная проверка |
| Посточная генерация через Faker | ~10^310^4 строк/с | бенчмарк mimesis |
| Сериализация orjson (запись ~50 КБ) | ~10^5 док/с | README orjson |
| Сериализация stdlib json (та же запись) | ~10^4 док/с | README orjson |
Ключевые множители: векторная генерация быстрее посточной на 1–2 порядка;
orjson быстрее stdlib json в 5–14 раз на сериализации; multiprocessing
умножает всё на число ядер, потому что обходит GIL подпроцессами.
Узкое место конвейера «сгенерировать → JSON в Kafka» — не случайные числа
(они почти бесплатны), а сборка посточных словарей и их сериализация:
это ~2–5×10^5 событий/с на ядро, дальше масштабирование только процессами.
## Находки по первоисточникам
### 1. orjson против stdlib json — бенчмарки автора
Источник: README orjson, раздел Performance —
<https://github.com/ijl/orjson#performance>. Среда автора: Python 3.11.10,
Fedora 42, x86-64-v4; скрипт `pybench` в репозитории.
Сериализация (медиана, операций в секунду):
| Файл | orjson | json | Ускорение |
|---|---|---|---|
| twitter.json (~600 КБ) | 8 453 | 765 | 11,1x |
| github.json (~55 КБ) | 103 693 | 7 648 | 13,6x |
| citm_catalog.json | 3 975 | 338 | 11,8x |
| canada.json | 399 | 33 | 11,9x |
Десериализация ускоряется скромнее: 2,2–6x на тех же файлах.
Отдельно важное для генератора: orjson сериализует `numpy.ndarray` нативно
(опция `OPT_SERIALIZE_NUMPY`). Числа автора: массив float64 на 92 МиБ —
105 мс против 1 481 мс у json (14,2x); int32 на 100 МиБ — 68 мс против
684 мс (10,1x). `datetime` тоже сериализуется нативно в RFC 3339, без
обёрток и `default=`.
Документация stdlib json заявлений о скорости не делает вовсе
(<https://docs.python.org/3/library/json.html>). Факт «чистый Python
с C-ускорителем» виден только в исходниках CPython: `Lib/json/encoder.py`
импортирует `c_make_encoder` из `_json` с откатом на Python-реализацию
(<https://github.com/python/cpython/blob/main/Lib/json/encoder.py>).
### 2. Векторная генерация случайных значений — документация numpy
Источник: официальная страница производительности генераторов —
<https://numpy.org/doc/stable/reference/random/performance.html>.
Среда: Linux, AMD Ryzen 9 3900X. Единицы — наносекунды на одно значение.
| Распределение | PCG64 | SFC64 | MT19937 |
|---|---|---|---|
| uint32 | 1,9 | 1,8 | 3,3 |
| uint64 | 3,2 | 2,5 | 5,6 |
| равномерное | 3,1 | 2,6 | 5,9 |
| нормальное | 10,8 | 8,3 | 13,9 |
| пуассоновское | 103,4 | 90,7 | 111,7 |
То есть 3 нс на равномерное значение — это ~3×10^8 значений/с на ядро.
Даже для широкого события в ~47 колонок чистая генерация значений даёт
миллионы строк в секунду: не она ограничивает конвейер. Рекомендация
numpy — PCG64 (или PCG64DXSM для сильно параллельных сценариев).
### 3. multiprocessing — документация Python
Источник: <https://docs.python.org/3/library/multiprocessing.html>.
Дословно: пакет «effectively side-stepping the Global Interpreter Lock by
using subprocesses instead of threads» — то есть генерация по независимым
модельным дням масштабируется числом ядер, а не упирается в GIL.
Практические указания из тех же доков: `Pool.map` режет итерируемое на
куски (`chunksize`), а для длинных последовательностей «using a large value
for chunksize can make the job complete much faster than using the default
value of 1» (про `imap`). Для нашей схемы «один процесс — один день» это
неактуально: единица работы и так крупная.
### 4. Faker и цена посточной генерации
Сам Faker чисел не публикует, но признаёт цену взвешенного выбора:
конструктор принимает `use_weighting`, и при `False` «the selection process
is much faster» (<https://faker.readthedocs.io/en/master/index.html>,
раздел Optimizations).
Числа даёт авторский бенчмарк mimesis против Faker —
<https://mimesis.name/master/benchmarks.html> (mimesis 19.0.0, MacBook Pro
M1 Pro). На 47 сопоставимых операциях mimesis выиграл все сравнения,
суммарное ускорение ~24x; по категориям — от 5x (Datetime) до 69x (Finance).
Порядок абсолютных цифр: сотни микросекунд на вызов Faker — то есть
~10^3–10^4 значений/с. Оговорка: единицы в таблицах страницы внутренне
противоречивы (µs против ms не сходятся с заявленным 24x), надёжны именно
коэффициенты ускорения, абсолютные значения — с осторожностью.
Вывод для нас: Faker в горячем цикле на десятки миллионов событий — это
узкое место на 2–3 порядка хуже numpy. Его место — генерация небольших
справочников (каталог, имена), не потока событий.
### 5. Кросс-проверка: msgspec
Бенчмарки автора msgspec (<https://msgspec.dev/benchmarks>, CPython 3.11):
msgspec со схемами быстрее всех, без схем — «on-par with orjson (the next
fastest JSON library)». Это подтверждает, что orjson — верхняя планка
скорости JSON в Python без введения схем; выигрыш msgspec — скорее память
(на декоде 77 МиБ: 67,6 МиБ пика против 406,3 МиБ у orjson).
## Локальная проверка порядков (не первоисточник)
Микробенчмарк на машине стенда (8 логических ядер, Python 3.14, `uv run
--with numpy --with orjson`), 200 тыс. строк по 15 колонок:
- numpy векторно: ~6,4 млн строк/с;
- посточно (stdlib random + dict): ~71 тыс. строк/с — разрыв ~90x;
- orjson.dumps по строке: ~560 тыс. строк/с; json.dumps: ~120 тыс. (4,7x);
- транспонирование «колонки → посточные dict» (нужно для JSON в Kafka):
~275 тыс. строк/с — именно оно, а не RNG, становится узким местом
векторного пути;
- multiprocessing, 4 процесса на независимых «днях»: x1,9 на маленькой
нагрузке (старт пула съедает долю; на крупных днях доля падает).
Локальные числа сходятся с первоисточниками по порядку величины: разрыв
orjson/json на маленьких записях меньше README-шного (4–5x против 11–14x —
у автора записи крупнее), остальное совпадает.
## Что это значит для генератора стенда
Без принятия решений — решает тикет #30; здесь только опора для него.
- Реалистичная планка конвейера «генерация + orjson посточно» —
~2–5×10^5 событий/с на ядро, с multiprocessing по дням — умножить на
число ядер. Например, 10 млн событий — это десятки секунд на одном ядре
и секунды на восьми.
- Случайные числа брать векторно из numpy (PCG64) — их стоимость можно не
учитывать в бюджете. Посточный цикл оставить только там, где логика
действительно посточная (цепочки сессий), и не звать в нём Faker.
- Живой поток ×60: даже 100 тыс. событий модельного дня при ×60 — это
~70 событий/с реального времени; на 3–4 порядка ниже планки, скоростью
не ограничен.
- Довод спеки «компилируемый язык только если замеры покажут, что Python
не тянет» получает численную опору: до ~10^6 событий/с суммарно Python
с батчевой архитектурой укладывается, дальше — территория Rust/Go.