Производительность: закон Амдала, CPI и бенчмарки

Урок учит честно мерить производительность процессора и понимать, почему «больше гигагерц» — не вся правда.

CPI (Cycles Per Instruction) — среднее число тактов на команду. Время выполнения = (число команд) × CPI × (длительность такта). Закон Амдала ограничивает ускорение от распараллеливания.

Зачем уметь мерить

«Этот процессор быстрее» — пустые слова без метрики. Производительность зависит от трёх множителей, и улучшение одного может ухудшить другой. Понимание формулы спасает от маркетинговых ловушек (например, «гигагерцевой гонки») и помогает оптимизировать код там, где это реально даёт эффект.

Основное уравнение производительности

  Время = (Число команд) x CPI x (Время такта)
            \___________/   \_/   \___________/
              компилятор/    микро-   частота
              алгоритм       архитект.

  быстрее = меньше команд, меньше CPI, или короче такт

Гигагерцы — это лишь «время такта». Процессор с меньшей частотой, но меньшим CPI (больше команд за такт) может быть быстрее. Посчитаем время для двух процессоров:

def exec_time(n_instr, cpi, clock_ghz):
    cycle_ns = 1 / clock_ghz          # длительность такта в нс
    return n_instr * cpi * cycle_ns   # время в нс

# программа из 1 млрд команд
N = 1_000_000_000
cpuA = exec_time(N, cpi=2.0, clock_ghz=3.0)   # высокая частота, высокий CPI
cpuB = exec_time(N, cpi=1.0, clock_ghz=2.5)   # ниже частота, лучше CPI

print(f"CPU A (3.0 ГГц, CPI 2.0): {cpuA/1e6:.0f} мс")
print(f"CPU B (2.5 ГГц, CPI 1.0): {cpuB/1e6:.0f} мс")
print("Быстрее:", "B" if cpuB < cpuA else "A", "- меньшая частота, но лучше CPI")

Вывод:

CPU A (3.0 ГГц, CPI 2.0): 667 мс
CPU B (2.5 ГГц, CPI 1.0): 400 мс
Быстрее: B - меньшая частота, но лучше CPI

Закон Амдала

Допустим, мы ускорили часть программы (например, распараллелили). Закон Амдала говорит: общий выигрыш ограничен той долей, которую нельзя ускорить. Если 10% программы строго последовательны, то даже с бесконечным числом ядер ускорение не превысит ×10.

def amdahl_speedup(parallel_fraction, n_cores):
    serial = 1 - parallel_fraction
    return 1 / (serial + parallel_fraction / n_cores)

p = 0.90    # 90% программы параллелится, 10% последовательны
print(f"Доля параллельного кода: {p*100:.0f}%")
for cores in (1, 2, 4, 16, 1000):
    s = amdahl_speedup(p, cores)
    print(f"  {cores:>4} ядер -> ускорение x{s:.2f}")
print("предел (бесконечно ядер):", round(1/(1-p), 1))

Вывод:

Доля параллельного кода: 90%
     1 ядер -> ускорение x1.00
     2 ядер -> ускорение x1.82
     4 ядер -> ускорение x3.08
    16 ядер -> ускорение x6.40
  1000 ядер -> ускорение x9.91
предел (бесконечно ядер): 10.0

Урок Амдала суров: 1000 ядер дают лишь ×9,91, потому что 10% кода остаются узким местом. Поэтому в оптимизации важно ускорять именно «горячую» большую часть, а не мелочь.

Осторожно с бенчмарками

Бенчмарк — стандартная программа для сравнения процессоров. Но цифры обманчивы: результат зависит от компилятора, настроек, типа задачи. Синтетический тест может не отражать вашу нагрузку. Правило: меряйте на своей реальной задаче, а к чужим «попугаям» относитесь скептически.

Глубже в тему

Главная ценность основного уравнения производительности (Время = Число команд × CPI × Время такта) в том, что оно разбивает скорость на три независимых множителя, за каждый из которых отвечает свой слой системы, — и это спасает от целого класса заблуждений. За число команд отвечают алгоритм и компилятор: лучший алгоритм или удачная оптимизация уменьшают сам объём работы. За CPI (такты на команду) отвечает микроархитектура: конвейер, суперскалярность, кэши, предсказатели — всё, чему был посвящён курс, нужно именно для снижения CPI. За время такта отвечает частота — те самые гигагерцы. Коварство в том, что множители не независимы в оптимизации: можно поднять частоту (уменьшить время такта), но углубление конвейера ради этого нередко ухудшает CPI из-за более дорогих сбросов. Уравнение учит смотреть на произведение, а не на отдельный множитель, — и в этом его отрезвляющая сила против маркетинга.

«Гигагерцевая гонка» начала 2000-х — отличный исторический урок, который прямо вытекает из уравнения. Маркетинг приучил покупателей мерить процессоры частотой, и производители гнались за гигагерцами, иногда ценой раздутого CPI (вспомним сверхглубокий конвейер Pentium 4). Расчёт из урока показывает суть: процессор B с меньшей частотой 2,5 ГГц, но вдвое лучшим CPI обгоняет процессор A на 3,0 ГГц, выполняя ту же программу за 400 мс против 667. Частота — лишь один из трёх множителей, и меньшая частота при лучшей микроархитектуре даёт меньшее итоговое время. Индустрия в итоге отказалась от гонки гигагерц в пользу IPC (величины, обратной CPI) и числа ядер, а потребительские названия процессоров перестали быть просто частотой. Тот, кто понимает уравнение, не купится на «больше гигагерц — значит быстрее».

Закон Амдала — это, пожалуй, самая важная и самая отрезвляющая идея всего раздела о параллелизме. Он гласит: если в программе есть доля, которую нельзя распараллелить, именно она ставит жёсткий потолок ускорению, сколько бы ядер вы ни добавили. Расчёт из урока бьёт наотмашь: при 90% параллелизуемого кода даже тысяча ядер даёт ускорение всего ×9,91, а теоретический предел — ×10, потому что оставшиеся 10% последовательного кода становятся неустранимым узким местом. Интуиция тут такая: распараллеливание сжимает параллельную часть почти до нуля, но последовательная часть остаётся как была, и в пределе именно она определяет время. Практический вывод драматичен: добавлять ядра имеет смысл лишь до тех пор, пока последовательная доля не начала доминировать; дальше деньги и энергия тратятся почти впустую. Поэтому в реальной оптимизации сначала ищут и сокращают последовательные узкие места, а уже потом масштабируют параллельную часть.

Закон Амдала дополняет более оптимистичный закон Густафсона, и понимание их спора делает картину полной. Амдал предполагает фиксированный размер задачи: ускоряем одну и ту же работу, и последовательная доля душит выигрыш. Густафсон заметил, что на практике, получив больше ядер, люди не ускоряют старую задачу, а берутся за бо́льшую: с ростом данных параллельная часть растёт, а последовательная (инициализация, ввод-вывод) остаётся примерно постоянной, поэтому её относительная доля падает, и масштабирование снова выгодно. Это объясняет, почему суперкомпьютеры с миллионами ядер реально полезны — на огромных задачах (прогноз погоды, симуляции, обучение больших нейросетей) параллельная доля близка к 100%. Что касается бенчмарков, оба закона напоминают об осторожности: производительность — это всегда «производительность на конкретной нагрузке». Синтетический тест с идеальным параллелизмом покажет одно, ваш ветвистый код с последовательными зависимостями — совсем другое. Поэтому главное правило измерений: мерьте на своей реальной задаче, а к чужим «попугаям» относитесь скептически, понимая, какой именно компромисс из трёхчленного уравнения они подсвечивают.

Частые ошибки

  • Судить о скорости по гигагерцам. Это лишь один из трёх множителей; CPI и число команд не менее важны.
  • Ждать линейного ускорения от ядер. Закон Амдала ограничивает выигрыш последовательной долей.
  • Доверять одному бенчмарку. Разные задачи дают разные результаты; меряйте свою нагрузку.

Итог

  • Время = число команд × CPI × время такта; частота — лишь один множитель.
  • Закон Амдала: ускорение ограничено непараллелизуемой долей программы.
  • Бенчмарки полезны, но коварны — мерьте реальную нагрузку.
Проверьте себя
1. Из каких трёх множителей складывается время выполнения программы?
AЧастота, объём ОЗУ, число ядер
BЧисло команд, CPI и длительность такта
CКэш, регистры, шина
DГигагерцы, ватты, биты
2. Что утверждает закон Амдала?
AУдвоение ядер всегда удваивает скорость
BОбщее ускорение ограничено долей программы, которую нельзя распараллелить
CПроизводительность растёт с частотой линейно
DКэш всегда ускоряет в 10 раз
3. Почему процессор с меньшей частотой может быть быстрее?
AЭто невозможно
BЕсли у него ниже CPI (больше команд за такт), он выполнит программу за меньшее время
CЕсли у него больше ватт
DЕсли у него меньше ядер