Суть Прочитай микробенчмарк, профайлер-дифф, вывод hardware counters perf и расчёт Amdahl, затем предскажи поведение и выбери фикс с наибольшим рычагом.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 14 min
Бенчмарки, профайлер-диффы и вывод counters — это где profile-first живёт или умирает. Прочитай каждый артефакт и выбери ход, который сеньор делает первым — до того, как тронуть ручку тюнинга или поверить headline-числу.
Цель
Потренируй цикл, который ты запускаешь в каждом расследовании производительности: прочитай бенчмарк или трейс, заметь ложь или сигнал и потянись к интерпретации с наибольшим рычагом прежде, чем что-либо менять.
Сниппет 1 — бенчмарк, который слишком быстр
func BenchmarkHash(b *testing.B) { data := []byte("fixed-input-string") for i := 0; i < b.N; i++ { _ = fnv32(data) // результат отброшен }}// reported: 0.31 ns/op — быстрее одной загрузки из памяти
Викторина
Completed
0.31 ns/op — ниже стоимости одной загрузки из L1. Что произошло и в чём фикс?
Heads-up 0.31 ns/op ниже одной загрузки из L1 (~1 нс); ни один реальный хеш byte-слайса так не работает. Компилятор удалил работу, потому что результат не используется — бенчмарк не измеряет ничего.
Heads-up testing.B наращивает b.N, пока прогон не станет достаточно длинным; точность не проблема. Проблема — dead-code elimination, удалившая тело целиком, плюс константный вход, который оптимизатор сворачивает.
Heads-up Профайлер не чинит бенчмарк, измеряющий удалённый код. Дефект в бенчмарке: потреби результат и используй неконстантный вход, чтобы работа реально выполнялась.
Сниппет 2 — профайлер-дифф против production-метрики
# Production (5-мин окно, после деплоя)checkout_p99_ms 580 (prev 820) # на 29% быстрееcpu_pct 62 (prev 58) # CPU ВЫРОС# go tool pprof -diff_base baseline.cpu prod.cpuShowing nodes accounting for -3.20s, 1.15% of -278.5s total flat flat% cum cum% -1.80s 0.64% -1.80s 0.64% net/http.(*conn).serve -1.40s 0.50% -1.40s 0.50% encoding/json.Marshal
Викторина
Completed
p99 упал на 29%, но дифф CPU-профиля показывает лишь ~1% чистого изменения CPU, а CPU% вырос. Как это примирить и что снимать дальше?
Heads-up Wall-clock задержка — это не только CPU. Для I/O- или lock-bound сервиса экономия — это off-CPU время ожидания, которое никогда не появляется в CPU-профиле. Метрики и крошечный CPU-дифф идеально согласованы.
Heads-up Более высокий CPU после фикса часто хороший сигнал: сервис делает реальную работу вместо ожидания. p99 улучшился на 29% — суди по SLO, а не по числу CPU в изоляции.
Heads-up Пропавшее время никогда не было на CPU, поэтому никакая частота сэмплинга не вытащит его в CPU-профиле. Нужен off-CPU профиль, чтобы увидеть фреймы DB-wait или mutex-wait.
Сниппет 3 — вывод hardware counters
$ perf stat -e cycles,instructions,cache-misses ./svc bench-json 142,310,884,001 cycles 61,994,210,773 instructions # 0.44 insn per cycle 1,902,544,118 cache-misses# flame graph: parseJSON — 35% CPU, единственный широкий лист
Викторина
Completed
parseJSON — 35% CPU. IPC 0.44 с огромным числом cache-misses. Какой фикс скорее поможет и какой — нет?
Heads-up IPC 0.44 говорит, что функция memory-stalled, а не compute-bound. Другой парсер, гоняющийся за теми же разбросанными указателями, увидит те же stalls. Рычаг — data layout, а не алгоритм.
Heads-up Векторизация помогает compute-bound коду (высокий IPC). При IPC 0.44 CPU простаивает в ожидании памяти; SIMD нечего ускорять, пока cache-misses не упадут.
Heads-up Вывод отмечает cache-misses, а не branch-misses. Низкий IPC плюс высокие cache-miss прямо указывают на memory stalls; проверь счётчик branch-miss отдельно прежде, чем предполагать его.
Сниппет 4 — решение по Amdahl
# Запрос всего: 200 мс. Профиль показывает:# funcA 100 мс (50%) -- опция 1: рерайт для 2x -> сэкономлено 50 мс# funcB 40 мс (20%) -- опция 2: рерайт B и C на 4x каждую# funcC 20 мс (10%) -- -> сэкономлено 45 мс суммарно
Викторина
Completed
Можно сделать опцию 1 ИЛИ опцию 2, не обе. Какая даёт больше общего speedup и каково общее правило?
Heads-up Величина локального speedup не решающий фактор; решает доля. 4x ложится лишь на 30% времени, экономя 45 мс, а 2x на 50% экономит 50 мс. Решает Amdahl, а не множитель.
Heads-up Они близки, но не равны: 50 мс против 45 мс, 1.33x против 1.29x. Суть именно в том, что надо посчитать Amdahl, а не прикидывать множители на глаз.
Heads-up Профиль уже дал тебе доли (p) и предложенные локальные speedup (s); это всё, что нужно Amdahl. Микробенчмарк лишь перемерил бы s в изоляции и не улучшил бы это решение.
Итог
Каждый артефакт здесь прячет ловушку или сигнал. Бенчмарк, чей результат отброшен, измеряет удалённый код, а не работу. Выигрыш p99 в 29% при плоском CPU-диффе означает, что экономия была off-CPU — сними wait-профиль, чтобы увидеть её. IPC ниже 1.0 с тяжёлыми cache-misses означает memory-stalled: чини data layout, а не алгоритм. А когда две оптимизации конкурируют, арифметика Amdahl по долям — не размер локального множителя — выбирает победителя. Прочитай число, найди ложь, затем действуй по доле.