- Зачем:
- холодное ревью (Fable, свежая сессия) нашло невыполненный хвост
тикета #32 и места, где доводы резолюций сжались до непонятности.
- Что:
- мастер-спека 1.1: «склад» заменён на «хранилище» (хвост #32);
CONTEXT.md: DWH в избегаемых, отдельная статья «Пакетный режим».
- спека: восстановлены доводы «на маке и в WSL тоже» и «менти упрётся
в красный чек манифеста»; обещания про diff привязаны к манифесту;
темп ×60 и расчёт порога согласованы с числами разделов; заголовок
притока честен про затухание; выход за мандат оговорён в «Зачем».
- раздел 9: добавлены числа притока и календарная дата-константа D0;
заметка исследования: у Faker единицы «значений/с», не «строк/с».
- Проверка:
- вычитка; решения развилок не пересматриваются, правки текстовые.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
160 lines
11 KiB
Markdown
160 lines
11 KiB
Markdown
# Скорость батчевой генерации событийных данных в 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^4–10^5 строк/с | локальная проверка |
|
||
| Посточная генерация через Faker | ~10^3–10^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.
|