awesome-everything EN
↑ Обратно к восхождению

Производительность

Branch prediction: 10–30 циклов штрафа за неожиданный if

Суть Modern branch predictors угадывают правильно в 95–99% случаев. Один неправильный guess на случайных данных — flush pipeline и 10–30 циклов простоя. Sorted-array trick, branchless код и PGO — три инструмента фикса.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на middle-высоте — в небе
◷ 13 min

Тот же цикл. Те же данные. Одно сортирование массива перед циклом — и код становится в 2–3 раза быстрее. Алгоритм не изменился. Изменился только порядок данных — и CPU больше не ошибается в предсказаниях.

Как branch prediction работает

Modern CPU pipeline глубокий: 15–20+ стадий. CPU fetch-ит инструкции задолго до их исполнения. Когда встречается branch (if, for, while, virtual call), CPU не знает заранее, какой путь будет taken — но он должен что-то поставить в pipeline.

Branch predictor угадывает: taken или not-taken. CPU начинает исполнять предсказанный путь спекулятивно. Если угадал правильно — win, pipeline не останавливался. Если ошибся — pipeline flush: все спекулятивно исполненные инструкции выбрасываются, нужно начать с правильного пути. Цена: глубина pipeline × missed slots.

На современном x86 с pipeline depth ~19 стадий: 10–30 wasted cycles per misprediction.

TAGE predictor

Modern branch predictors умные. Intel и AMD используют TAGE (TAgged GEometric) predictor, комбинирующий несколько history length. Он запоминает не просто last-taken/not-taken, а complex patterns up to N branches назад.

Типичная accuracy: 95–99% на реальном коде.

Hard cases для любого predictor:

  • Data-dependent branches (if на random input value).
  • Virtual function calls с высокой polymorphism (непредсказуемый target).
  • Очень длинные pipelines (Apple M1/M2/M3: 10+ стадий) — ошибка дороже.

Sorted-array trick

Проблема: tight loop суммирует элементы array, превышающие threshold.

int sum = 0;
for (int i = 0; i < N; i++) {
    if (arr[i] > threshold) sum += arr[i];  // branch 50/50 на random data
}

На random unsorted data: ~50% miss rate → ~15 wasted cycles per iteration.

Фикс: сортировка перед циклом.

std::sort(arr, arr + N);  // O(N log N) один раз
for (int i = 0; i < N; i++) {
    if (arr[i] > threshold) sum += arr[i];
}

Теперь первая половина цикла всегда not-taken (arr[i] <= threshold), вторая всегда taken. Predictor учит pattern быстро: один transition point, 99%+ accuracy после него.

Несмотря на O(N log N) sort cost, общее время часто меньше — потому что inner loop перестаёт платить mispredict penalty на каждой итерации.

Branchless код

Когда branch непредсказуем и нельзя re-sort данные — убирай branch совсем.

// branched (mispredict-friendly если arr[i] > 0 random):
if (arr[i] > 0) y = a; else y = b;

// branchless:
y = (arr[i] > 0) * a + (arr[i] <= 0) * b;

Или CMOV (conditional move на x86) — компилятор часто emit-ит автоматически при -O2:

y = arr[i] > 0 ? a : b;  // компилятор может генерировать cmov

Tradeoff:

  • Branchless версия всегда вычисляет обе стороны.
  • Branched версия пропускает одну сторону но платит mispredict cost когда ошибается.

Tipping point: если branch предсказуем 95%+, branched быстрее. Если 50/50 (random), branchless выигрывает в 2–5x.

Предсказуемость branchРекомендацияПочему
95%+ предсказуемОставь branchedPredictor угадывает правильно, branch почти бесплатен
~80% предсказуемРассмотри branchless20% miss rate × 15 циклов = значительно
~50/50 (random)Branchless / re-sort50% miss rate + flush pipeline каждые 2 итерации

PGO для branches

Profile-guided optimisation (PGO): build с instrumentation, run representative workload, recompile с profile данными.

PGO для branches:

  • Predicted-taken branches размещаются последовательно в памяти — hot path contiguous в instruction cache.
  • Basic blocks переупорядочиваются, держа hot path вместе.
  • Компилятор инлайнит callees, которые hot, и не инлайнит cold.

Real speedup: 10–30% на большинстве production workloads. Компилятор делает автоматически то, что разработчик иначе делает через __builtin_expect и ручное аннотирование.

Branch prediction числа
Pipeline depth (x86)
~19 стадий
Pipeline depth (Apple M1/M2)
10+ стадий
Misprediction penalty
10–30 циклов
Zen 2 pipeline depth
19 стадий
TAGE predictor accuracy
95–99% real code
50/50 miss rate speedup от branchless
2–5x
PGO speedup
10–30%
Почему это работает

Compiler annotations (__builtin_expect, [[likely]], [[unlikely]]) — лёгкий способ подсказать predictor без PGO. Но они на best-guess разработчика; PGO опирается на реальный runtime data. Для production-critical inner loops PGO точнее. Для startup code или rarely-executed paths — compiler hints достаточно.

Викторина

Loop имеет if-branch, идущий true 50% времени, непредсказуемо. Какое типичное perf-влияние?

Викторина

Команда использует sorted-array trick на inner loop с if-branch на random data. Почему это помогает, несмотря на O(N log N) sort cost?

Расставь шаги по порядку

Поставь события pipeline flush при misprediction:

  1. 1 CPU fetch-ит branch инструкцию в pipeline
  2. 2 Branch predictor угадывает: 'not-taken'
  3. 3 CPU спекулятивно fetch-ит и начинает execute 'not-taken' путь
  4. 4 Execution достигает branch: реальный результат 'taken'
  5. 5 Misprediction detected: все спекулятивные инструкции выбрасываются (pipeline flush)
  6. 6 CPU начинает заново с 'taken' пути — 10–30 циклов потеряно
Вспомните перед уходом
  1. 01
    Тот же loop с branch на random data — 50% miss rate. Как sorted-array trick фиксит без изменения алгоритма?
  2. 02
    Когда branchless код быстрее, чем branched с данными-зависимым if?
Итог

Modern CPU pipeline спекулятивно fetch-ит инструкции по предсказанному пути branch. При misprediction — pipeline flush, 10–30 циклов wasted. TAGE predictor достигает 95–99% accuracy на предсказуемых данных. На random data (sort, filter, packet classification) — 50% miss rate, dominant bottleneck. Три fix-паттерна: (1) sorted-array trick — превращает random в monotone pattern, predictor выучивает за несколько итераций; (2) branchless код — CMOV или arithmetic select, всегда вычисляет обе стороны, но избегает flush; (3) PGO — bake-ит runtime frequencies в code layout, hot branches sequential в memory. Диагностика через branch-misses counter в perf stat.

Связанные уроки
встречается в159
Продолжить восхождение ↑SIMD и data layout: AoS vs SoA и разница в 4–8x
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.