Производительность: закон Амдала, 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 × время такта; частота — лишь один множитель.
- Закон Амдала: ускорение ограничено непараллелизуемой долей программы.
- Бенчмарки полезны, но коварны — мерьте реальную нагрузку.