Производительность
История профайлеров и ловушки микробенчей: от Кнута до GWP
Дефолтный warmup JMH для Java — 5 итераций. Он существует не ради производительности — он нужен потому, что без него твой бенчмарк измеряет интерпретатор, а не production JIT. Большинство команд, впервые самостоятельно пишущих микробенчмарки, делают эту ошибку с первого прогона.
Интеллектуальная родословная: от Амдала до GWP
Дисциплина «профиль первым» имеет 60-летнюю родословную.
Джин Амдал, 1967 — «Validity of the Single Processor Approach to Achieving Large Scale Computing Capabilities». Показал, что последовательная часть нагрузки ограничивает ускорение, которое способно дать любое количество параллельных процессоров. Исходный аргумент за измерение доли перед оптимизацией.
Дональд Кнут, 1974 — «Structured Programming with Go To Statements», ACM Computing Surveys. Ввёл формулировку «преждевременная оптимизация — корень всех зол», с полным предложением, называющим разделение 97/3: большая часть кода не имеет значения; критическая доля имеет, и её идентификация — это и есть инженерная работа.
Джон Густафсон, 1988 — «Reevaluating Amdahl’s Law». Утверждал, что по мере роста размера задач параллелизуемая доля растёт, так что пессимизм Амдала занижает достижимое ускорение на реальных нагрузках. Закон Густафсона: масштабированное_ускорение = s + p × N, где N — количество процессоров, а нагрузка растёт вместе с N.
Google Wide Profiling (GWP), 2010 — Рен и соавторы, IEEE Micro. Показали непрерывное профилирование всего датацентра при накладных расходах менее 0.01% с помощью статистического мультиплексирования. Эта техника стала Pyroscope, Parca, Polar Signals и категорией непрерывного профилирования.
Дуга: идентифицируй узкое место (Амдал 1967) → назови это дисциплиной (Кнут 1974) → уточни модель при масштабировании (Густафсон 1988) → сделай измерение бесплатным и постоянным (GWP 2010, Pyroscope 2020-е). Каждый слой добавил ограничение, ставшее стандартной практикой.
Ловушки микробенчей в JIT-рантаймах
Наивные микробенчи почти по умолчанию неверны в JIT-компилируемых рантаймах (JVM, V8, .NET CLR, PyPy).
Проблема JIT warmup. Ступенчатая компиляция HotSpot: ~10k вызовов для C1 (базовый), ~100k для C2 (оптимизированный). Микробенч, вызывающий функцию 1k раз, измеряет интерпретатор или базовый JIT, а не оптимизированный код, работающий в продакшене. JMH справляется с этим явными warmup-итерациями (по умолчанию 5 итераций × 10 с каждая до начала измерения).
Устранение мёртвого кода. Если результат бенчмарка не используется, оптимизатор полностью удаляет тело цикла. Цикл выполняется за микросекунды и репортирует невозможное ускорение. testing.B в Go требует записи результата в sink-переменную пакета (var sinkResult = result). JMH использует Blackhole.consume(result).
Свёртка констант. Если входные данные цикла — константы времени компиляции, оптимизатор вычисляет ответ один раз и заменяет цикл литералом. Цикл, вычисляющий md5("fixed-string") 1M раз, может свернуться в единственную загрузку константы. Решение: параметризовать входные данные во время выполнения из непостоянного источника (JMH @Param, Go benchmark + внешние данные).
Различия в инлайнинге. Микробенч может инлайнить функцию, которую продакшен не инлайнит (или наоборот), потому что дерево вызовов бенчмарка проще. Аннотации @CompilerControl в JMH позволяют принудительно включать или отключать инлайнинг для соответствия production-поведению.
Масштабирование частоты CPU. CPU ноутбуков агрессивно троттлятся: время бенчмарка функции варьируется на 30% между холодным стартом и троттлингом. Production-железо имеет другие политики частоты. Всегда бенчмаркируй на представительном железе с отключенным масштабированием частоты или зафиксированными тактами.
| Ловушка | Симптом | Фикс |
|---|---|---|
| JIT warmup | Бенчмарк в 3-10x медленнее продакшена | Явный warmup (JMH), B.ResetTimer() после прогрева в Go |
| Устранение мёртвого кода | Бенчмарк завершается за наносекунды — подозрительно быстро | Консьюмить результат через sink-переменную или Blackhole |
| Свёртка констант | Время инвариантно к размеру входных данных | Параметризовать входные данные во время выполнения, не на этапе компиляции |
| Различия в инлайнинге | Бенч в 2x быстрее продакшена | @CompilerControl для принудительного/запрещённого инлайнинга |
| Масштабирование частоты CPU | Высокая дисперсия между прогонами на ноутбуке | Зафиксировать такты CPU, бенчмаркировать на серверном железе |
Почему это работает
Промышленные фреймворки (JMH для Java, criterion.rs для Rust, benchstat в Go) стандартизируют прогрев, запускают несколько итераций и репортируют статистические сводки с предупреждениями о дисперсии. Написание микробенчмарка с нуля в спешке — самодельный цикл таймирования — обнаружит одну или несколько описанных выше ловушек. Используй фреймворк; не изобретай заново.
Где была введена каноническая формулировка «преждевременная оптимизация — корень всех зол», и какова ПОЛНАЯ цитата?
Микробенчмарк запускает целевую функцию 500 итераций и репортирует среднее время на вызов. Метод на Java. Наиболее вероятный дефект этой установки?
- 01Разбери четыре ловушки микробенчей в JIT-рантаймах и фикс для каждой.
- 02Изложи интеллектуальную родословную от Амдала 1967 до Google Wide Profiling 2010 в четырёх шагах.
Дисциплина «профиль первым» имеет 60-летнюю родословную. Амдал (1967) количественно обосновал потолок оптимизации. Кнут (1974) назвал дисциплину идентификации критических 3%. Густафсон (1988) скорректировал Амдала для масштабируемых нагрузок. GWP (2010) сделал непрерывное профилирование достаточно дешёвым для постоянного production-деплоя. Наивные микробенчи в JIT-рантаймах по умолчанию измеряют не то: JIT warmup означает, что работает интерпретатор, а не оптимизированный код; устранение мёртвого кода удаляет тело цикла; свёртка констант заменяет циклы литералами; масштабирование частоты CPU смещает измерения на ноутбуках. Промышленные фреймворки (JMH, criterion.rs, benchstat в Go) справляются со всем этим — используй их вместо самодельных циклов таймирования.
встречается в159
- Путь запроса: семь остановок от сокета до ответаjunior
- Accept и парсинг: от очереди ядра до типизированного запросаmiddle
- Маршрутизация и middleware: что выполняется и в каком порядкеmiddle
- Обработчик и ответ: от бизнес-логики до байтов на проводеmiddle
- Стриминг и backpressure: когда клиент читает медленнее, чем вы пишетеsenior
- Таймауты и хвостовая задержка: бюджеты, дедлайны и ловушка fan-outsenior
- Middleware и DI: два паттерна, формирующие любой backendjunior
- Пишем middleware: сигнатуры, next() и три модели фреймворковmiddle
- Инверсия управления: как зависимости добираются до классаmiddle
- Скоупы и время жизни DI: singleton, request, transientmiddle
- DI как шов для тестов: фейки, моки и граница, которая важнаsenior
- DI-контейнеры в продакшене: графы разрешения, циклы и когда не стоитsenior
- Блокирующий vs неблокирующий I/O: два способа ждатьjunior
- Event loop: один поток, упорядоченные фазыmiddle
- Что блокирует цикл: CPU-работа и синхронные вызовыmiddle
- Вынос CPU-работы: worker threads и пул libuvmiddle
- Backpressure и ограниченная конкурентностьsenior
- Пропускная способность под нагрузкой: хвостовая задержка и насыщениеsenior
- Зачем пул: цена создания соединенияjunior
- Размер пула: почему больше не значит быстрееmiddle
- Взятие и таймауты: очередь ожидания — настоящий дроссель задержкиmiddle
- Стратегии retry: backoff, jitter и thundering herdmiddle
- Наблюдаемость, production-инциденты и дизайн для глобального масштабаsenior
- Задачи, микрозадачи и scheduler.yield()middle
- Точность таймеров, троттлинг и фоновая работаmiddle
- Event loop Node.js: фазы, nextTick и задержка циклаsenior
- Стратегии рендеринга: SSG, SSR, ISR, streaming и гидратацияjunior
- SSG, SSR, ISR, streaming и RSC — как работает каждая стратегияmiddle
- Цена гидратации: selective, progressive, острова, resumabilitymiddle
- Core Web Vitals: что измеряют LCP, INP и CLSjunior
- LCP: четыре фазы, одна доминирующая стоимостьmiddle
- INP: input delay, processing, presentationmiddle
- Lab vs field: почему они расходятся и как использовать каждыйmiddle
- Трейдоффы метрик, RUM-атрибуция и цикл CI+полеsenior
- Общая картина: от URL до LCP до INP как эстафетаjunior
- Восемь слоёв трассировки: от service worker до второй навигацииmiddle
- Пять канонических поломок: где производство стабильно ломаетсяsenior
- Метод трёх треков: чтение трасс и построение системы мониторингаsenior
- Что такое индекс и как он ускоряет запросыjunior
- Leading-column rule: почему порядок столбцов в composite-индексе важенmiddle
- Partial, expression и covering-индексыmiddle
- Типы индексов: GIN, GiST, BRIN, Hash, Bloom и HOT-обновленияmiddle
- Index-only scan, Visibility Map и INCLUDEsenior
- Типичные сбои в продакшне и аудит индексовsenior
- Упражнение по проектированию индексов: стратегия полнотекстового поискаsenior
- EXPLAIN и планы выполнения: что решает планировщик и почемуjunior
- Типы сканирования: Seq, Index, Bitmap, Index-Onlymiddle
- Алгоритмы соединения и каскад ошибок оценки строкmiddle
- pg_statistic, ANALYZE и производственная наблюдаемостьmiddle
- Расширенная статистика: исправление ошибок оценки для коррелированных колонокsenior
- Кеш планов, настройка константных стоимостей и внутренности планировщикаsenior
- Производственные режимы отказа и стабильность плановsenior
- Connection pool: зачем амортизировать стоимость backend Postgresjunior
- Режимы PgBouncer: session, transaction и statementmiddle
- Размер пула: формула (ядра × 2) + шпинделей и двухуровневый стекmiddle
- Исчерпание пула и idle-in-transaction: сценарий отказа в 3 ночиmiddle
- Миграция на transaction mode: план развёртывания и prepared statements в PgBouncer 1.21middle
- Процессная модель Postgres и почему увеличение max_connections снижает производительностьsenior
- Ландшафт пулеров 2026, serverless connection storms и полная таксономия отказовsenior
- ADD COLUMN: мгновенно в PG 11+ против перезаписи в старом Postgresjunior
- Режим отказа очереди блокировок: почему мгновенный DDL может заморозить базуmiddle
- Безопасные DDL-паттерны: NOT VALID, CONCURRENTLY и исправления небезопасных операцийmiddle
- Таксономия сбоев миграций и дисциплина продакшнаsenior
- Выбор ключа шарда: стратегии hash, range, list и directorymiddle
- Ко-локация и Citus: инвариант, делающий шардирование пригодным к использованиюmiddle
- Режим отказа hot shard: обнаружение, изоляция и долгосрочная политикаmiddle
- Онлайн-решардинг, 2PC и операционная стоимость шардированияsenior
- Семь актов: от CREATE TABLE до Citusjunior
- Акты 1–3 в глубину: схема, индексы и статистика планировщикаmiddle
- Акты 4–6 в глубину: MVCC bloat, connection pooling и безопасные миграцииmiddle
- Акт 7 в глубину: шардинг, co-location и семиуровневый каскад трейдоффовmiddle
- Наблюдаемость, антипаттерны и производственный триажsenior
- Биты в проводеjunior
- Математика задержкиmiddle
- Bufferbloat и перегрузкаsenior
- Граница физического уровняsenior
- Номера последовательности и состояние соединенияmiddle
- Управление потоком и перегрузкойmiddle
- BBR, производственная наблюдаемость и за пределами TCPsenior
- CDN: контент по соседствуjunior
- Anycast и GeoDNS: маршрутизация к ближайшему edgemiddle
- Многоуровневый кеш и Cache-Controlmiddle
- Заголовок Vary и cache keysmiddle
- Stale-while-revalidate и cache stampedesenior
- Edge workers и edge-side compositionsenior
- CDN: операции и observabilitysenior
- WebSocket: HTTP-апгрейд до постоянного соединенияjunior
- WebSocket vs SSE vs long-polling: выбор правильного транспортаmiddle
- Backpressure в WebSocket: когда клиенты не успеваютmiddle
- Реконнект: jittered backoff, thundering herd, восстановление сообщенийsenior
- WebSocket в масштабе: HTTP/2 мультиплексирование, permessage-deflate, C10Msenior
- WebSocket в production: прокси, безопасность и распределённая архитектураsenior
- Что делают обратные проксиjunior
- Алгоритмы балансировки: от round-robin до power-of-two-choicesmiddle
- L4 vs L7 балансировка и сохранение IP клиентаmiddle
- Health checks, connection draining и slow startmiddle
- Retry-бури, circuit breakers и load sheddingsenior
- Устойчивая архитектура LB: anycast, zone-aware маршрутизация и observabilitysenior
- Почему QUIC, а не TCP+TLSjunior
- QUIC-потоки и head-of-line blockingjunior
- Объединённое рукопожатие и 1-RTTmiddle
- Connection ID и миграция сетиmiddle
- Обнаружение потерь и управление перегрузкойmiddle
- Возобновление 0-RTT и шифрование пакетовsenior
- Развёртывание и стоимость CPUsenior
- DDoS: что это и почему работаетjunior
- Атаки усиления и истощение состоянияmiddle
- Ограничение скорости: алгоритмы и архитектураmiddle
- WAF, межсетевые экраны, mTLS и HSTSmiddle
- Отравление DNS-кэша и BGP-перехватsenior
- Эшелонированная защита и экономика атакsenior
- Двенадцать слоёв: один URL, семь действующих лицjunior
- DNS, TCP, TLS по очереди: куда уходят миллисекундыmiddle
- Критический путь рендеринга и Core Web Vitalsmiddle
- Перехват прокси и шлюзы безопасности: rate limiter, WAF, mTLSmiddle
- Альтернативные пути: QUIC 0-RTT, WebSocket upgrade, миграция соединенияmiddle
- Наблюдаемость: распределённые трейсы, USE/RED и семплированиеsenior
- Устойчивость: каскадные повторы, circuit breakers и error budgetsenior
- Что такое три сигнала: метрики, логи, трейсыjunior
- Метрики и cardinality: cost-модель time-series databasemiddle
- Логи и объём: cost-модель структурного логированияmiddle
- Трейсы и сэмплирование: cost-модель distributed tracingmiddle
- Join-ключи и exemplar''''ы: как три сигнала становятся компонуемымиmiddle
- Observability 2.0: широкие события и сдвиг стоимостиsenior
- Режимы сбоя и инженерная практика: cardinality budget''''ы, PII и сэмплированиеsenior
- Зачем нужны структурные логи: дневник против таблицыjunior
- Схема продакшн-лога: поля, которые несёт каждая строкаmiddle
- Log levels и маршрутизация алертовmiddle
- Стратегии sampling и стоимость логовmiddle
- PII-редакция и log injectionsenior
- Propagation trace-контекста в логахsenior
- OTel Logs Data Model и audit-логи как подсистемаsenior
- Сигналы OTel, Semantic Conventions и проводной формат OTLPmiddle
- Авто-инструментирование и ручные спаны: правило 80/20 в OTelmiddle
- Collector OTel: receivers, processors, exporters и паттерны развёртыванияmiddle
- Стратегии сэмплирования: head, tail и parent-basedmiddle
- Vendor-нейтральность, eBPF-инструментирование, Operator и OTel в браузере и serverlesssenior
- Эксплуатация OTel Collector: надёжность, version skew, режимы отказа и управлениеsenior
- RED и USE: два чек-листа, одна дисциплина триажаjunior
- Инструментация RED в Prometheus: счётчики, гистограммы и дисциплина cardinalitymiddle
- USE на Linux: CPU, память, диск, сеть и PSImiddle
- Golden signals, структура дашборда и auto-RED в service meshmiddle
- Cardinality как драйвер затрат: label, PII, exemplars и семплированиеmiddle
- Native histograms, SLO и паттерны production-сбоевmiddle
- Выбор SLI и SLO-целей: отношения, не ощущенияmiddle
- Multi-window multi-burn-rate-алертинг: почему AND лучше ORmiddle
- Error budget policy, latency SLO и составные journeysmiddle
- Iceberg SLI, математика составного SLO и SLA vs SLOsenior
- Flame graph: читаем картинку, которая показывает, куда ушло времяjunior
- Sampling vs instrumentation profiling: почему 99 Гц побеждает в productionmiddle
- Типы профилей: CPU, память, off-CPU, mutex — какой когда братьmiddle
- Continuous profiling: always-on flame graphs с eBPF и корреляцией trace-idmiddle
- Как flame graph строится из сэмплов и как использовать его в productionmiddle
- Linux perf, внутренности eBPF, PGO и ограничения sampling''''аsenior
- Profiling в production: безопасность, war stories, OTel profiles и дизайн инфраструктурыsenior
- Debugging-воронка: SLO → RED → trace → profilejunior
- Архитектура OTel: один SDK, четыре сигнала, один wire-форматmiddle
- Экономия на observability: удерживаем затраты в пределах 5% inframiddle
- Масштаб, безопасность и ROI наблюдаемых системsenior