В чём суть многоядерности?
Когда улучшение производительности одноядерных процессоров столкнулось с узкими местами (энергетическая стена, частотная стена, стена сложности конвейера), инженеры придумали «простое и грубое», но эффективное решение: разместить несколько ядер процессора на одном кристалле. В этой лекции будут представлены принципы и преимущества многоядерности, а также ограничения закона Амдала на параллельное ускорение.
Бытовая аналогия: кухня ресторана быстрого питания
Представьте ресторан быстрого питания в обеденный час пик.
Одноядерный режим: Только один повар. Он отчаянно пытается ускориться (повысить частоту) и оптимизировать процесс приготовления (конвейер), но как бы он ни оптимизировал, за раз можно приготовить только одно блюдо. Клиенты ждут в длинной очереди и теряют терпение.
Многоядерный режим: Начальник нанимает четырёх поваров, у каждого своя плита. Четыре повара могут одновременно готовить четыре разных блюда. Каждому повару не нужно становиться быстрее, но общая производительность увеличивается в несколько раз.
Это философия проектирования многоядерности:Когда одно ядро трудно ускорить дальше, лучше добавить больше ядер и позволить им работать параллельно.
Многоядерность не означает «сделать 4 копии всего». 4 ядра обычно совместно используют одну и ту же память, один и тот же дисковый интерфейс и одну и ту же операционную систему; они дублируются только на уровне «вычислительного блока». Точно так же, как 4 повара, совместно использующие один холодильник и один склад ингредиентов.
Одноядерные и многоядерные: сравнение архитектуры
Дилемма одноядерных процессоров
В начале 2000-х индустрия процессоров повышала производительность, постоянно увеличивая частоту — с 1 GHz до 2 GHz и до 3 GHz. Но этот путь быстро исчерпал себя.
Три основных узких места частоты одноядерных процессоров:
- стена энергопотребления: Чем выше частота, тем экспоненциальнее растёт энергопотребление, а отвод тепла становится физическим пределом.
- стена памяти: Процессор слишком быстр, память не успевает за ним, и большую часть времени он простаивает в ожидании данных.
- Стена ILP: параллелизм на уровне инструкций (ILP) был использован до предела; всё меньше инструкций в одном потоке инструкций поддаётся распараллеливанию.
Примерно в 2005 году Intel и AMD последовательно отказались от чистой «гонки частот» и перешли на многоядерный путь.
Структура многоядерного процессора
Типичная внутренняя структура многоядерного процессора выглядит следующим образом:
+-----------------------------------------------------------+ | CPU 芯片 | | +-------------+ +-------------+ +-------------+ +-----+ | | 核心 1 | | 核心 2 | | 核心 3 | |核 4 | | | L1 Cache | | L1 Cache | | L1 Cache | | ... | | | L2 Cache | | L2 Cache | | L2 Cache | | | | +------+------+ +------+------+ +------+------+ +--+--+ | | | | | | +----------------+----------------+--------------+ | | | +-----+------+ | | 共享 L3 Cache | | +-----+------+ | | | +-----+------+ | | 内存控制器 | ← 连接主内存 (RAM) | +------------+ +-----------------------------------------------------------+
Ключевые особенности архитектуры:
- Каждое ядро имеет собственные кэши L1 и L2 (приватные).
- Несколько ядер совместно используют один кэш L3
- Все ядра обращаются к одной и той же основной памяти через контроллер памяти.
- Операционная система отвечает за распределение задач между разными ядрами.
Закон Амдала: предел ускорения от многоядерности.
Интуитивное представление, что «больше ядер — значит быстрее», не всегда верно. В 1967 году специалист по информатике Джин Амдал предложил знаменитый закон.
Закон Амдала: общее ускорение задачи ограничено той частью, которая должна выполняться последовательно.
Формула выглядит следующим образом:
加速比 = 1 / (S + P/N) 其中: S = 任务中必须串行执行的比例(0~1) P = 任务中可以并行执行的比例(0~1),且 S + P = 1 N = 并行处理单元的数量(核心数)
Эта формула раскрывает жестокий факт:
Даже при бесконечном числе ядер, если распараллелить можно только 50% задачи, ускорение составит не более 2x.
Вывод: когда N стремится к бесконечности, P/N стремится к 0, а ускорение стремится к 1/S.
| Доля распараллеливаемой части | 1 ядро | 2 ядра | 4 ядра | 8 ядер | 16 ядер | Бесконечное число ядер (верхний предел) |
|---|---|---|---|---|---|---|
| 50% | 1.00x | 1.33x | 1.60x | 1.78x | 1.88x | 2.00x |
| 75% | 1.00x | 1.60x | 2.29x | 2.91x | 3.37x | 4.00x |
| 90% | 1.00x | 1.82x | 3.08x | 4.71x | 6.40x | 10.00x |
| 95% | 1.00x | 1.90x | 3.48x | 5.93x | 9.14x | 20.00x |
| 99% | 1.00x | 1.98x | 3.88x | 7.48x | 13.91x | 100.00x |
Обратите внимание на закономерность в этой таблице:
- Чем выше доля параллелизма, тем значительнее ускорение на многоядерных системах.Задачу, которая распараллеливается на 99%, можно ускорить почти в 14 раз на 16 ядрах.
- Чем больше ядер, тем меньше предельная отдача.Прирост ускорения при переходе с 2 ядер на 4 ядра намного больше, чем с 16 ядер на 32 ядра.
- Последовательная часть — это жёсткое ограничениеКогда только 50% поддаётся распараллеливанию, даже с десятью тысячами ядер ускорение может быть не более чем в 2 раза.
Закон Амдала объясняет, почему "больше ядер не обязательно означает быстрее". Если ваша программа в основном состоит из последовательной логики — например, этап вычислений должен ждать результата предыдущего этапа — то сколько бы ядер ни было, они могут только наблюдать и не могут помочь.
Интерактивная демонстрация: калькулятор закона Амдала
Приведённый ниже код на Python вычисляет и выводит сравнительную таблицу ускорений согласно закону Амдала, а также строит график того, как ускорение меняется с числом ядер, чтобы вы могли наглядно ощутить влияние "последовательного узкого места".
Пример
Калькулятор закона Амдала (демонстрационный пример)
Рассчитайте ускорение при различных долях параллельной части и количестве ядер
Вывести сравнительную таблицу и анализ тенденций
"""
def amdahl_speedup(parallel_fraction, num_cores):
"""
Основная формула закона Амдала
parallel_fraction: доля, которая может выполняться параллельно (0.0 ~ 1.0)
num_cores: количество ядер
Вернуть ускорение
"""
if num_cores == 1:
return 1.0
serial_fraction = 1.0 - parallel_fraction
speedup = 1.0 / (serial_fraction + parallel_fraction / num_cores)
return speedup
def amdahl_limit(parallel_fraction):
"""
Вычислить теоретический верхний предел ускорения по закону Амдала (при стремлении числа ядер к бесконечности)
Когда N→∞, P/N→0, ускорение → 1/S
"""
serial_fraction = 1.0 - parallel_fraction
if serial_fraction == 0:
return float('inf') # Полностью параллелизуемая, теоретически может быть ускорена бесконечно
return 1.0 / serial_fraction
def print_comparison_table(parallel_fractions, core_counts):
"""Вывести полную таблицу сравнения ускорения."""
# Заголовок таблицы
header = f"{'Ядра':>6} |"
for pf in parallel_fractions:
header += f" {pf*100:>5.0f}% параллелизуемо |"
print("=" * (12 + 12 * len(parallel_fractions)))
print(Закон Амдала - Таблица сравнения ускорения для многоядерных систем)
print("=" * (12 + 12 * len(parallel_fractions)))
print(header)
print("-" * (12 + 12 * len(parallel_fractions)))
for cores in core_counts:
row = f" {cores:>5} |"
for pf in parallel_fractions:
speedup = amdahl_speedup(pf, cores)
row += f" {speedup:>6.2f}x |"
print(row)
# Теоретический верхний предел
print("-" * (12 + 12 * len(parallel_fractions)))
limit_row = f" {'Верхний предел':>5} |"
for pf in parallel_fractions:
limit = amdahl_limit(pf)
if limit == float('inf'):
limit_row += f" нет верхнего предела |"
else:
limit_row += f" {limit:>6.2f}x |"
print(limit_row)
print("=" * (12 + 12 * len(parallel_fractions)))
def analyze_diminishing_returns(parallel_fraction, max_cores=64):
"""Проанализировать убывающую предельную отдачу от многоядерного ускорения"""
print(f"\nАнализ предельной выгоды (доля параллелизуемой части {parallel_fraction*100:.0f}%):)
print(f{'Ядра':>6} | {'Ускорение':>8} | {'Прирост':>10} | {'Эффективность':>8})
print("-" * 42)
prev_speedup = 1.0
cores = 1
while cores <= max_cores:
speedup = amdahl_speedup(parallel_fraction, cores)
improvement = speedup - prev_speedup
# Эффективность = фактическое ускорение / количество ядер (100% при идеальном линейном ускорении)
efficiency = (speedup / cores) * 100
print(f" {cores:>6} | {speedup:>8.2f}x | +{improvement:>8.3f}x | {efficiency:>7.1f}%")
prev_speedup = speedup
if cores == 1:
cores = 2
else:
cores *= 2
def compare_serial_vs_parallel(serial_work_ms, parallel_work_ms, num_cores):
"""
Используйте время, затраченное на конкретную задачу, чтобы продемонстрировать закон Амдала
serial_work_ms: время, затраченное на последовательную часть (миллисекунды)
parallel_work_ms: время параллелизуемой части (на одном ядре, миллисекунды)
num_cores: количество доступных ядер
"""
total_single_ms = serial_work_ms + parallel_work_ms
parallel_fraction = parallel_work_ms / total_single_ms
parallel_time_multi = parallel_work_ms / num_cores
total_multi_ms = serial_work_ms + parallel_time_multi
speedup = total_single_ms / total_multi_ms
print(f"\nМоделирование длительности конкретной задачи:)
print(fВремя последовательной части: {serial_work_ms} ms (не может быть ускорена))
print(fВремя, затраченное на параллелизуемую часть: {parallel_work_ms} ms (на одном ядре))
print(fДоля параллельной части: {parallel_fraction*100:.0f}%)
print(f"")
print(fОбщее время на одном ядре: {total_single_ms} ms)
print(f" {num_cores} ядер, общее время: {total_multi_ms:.1f} ms(последовательно {serial_work_ms} + параллельно {parallel_work_ms}/{num_cores} = {parallel_time_multi:.1f})")
print(f" Фактическое ускорение: {speedup:.2f}x")
print(fПредел Амдала: {amdahl_limit(parallel_fraction):.2f}x)
return speedup
# ===== Основная программа =====
print("=" * 60)
print(Калькулятор закона Амдала (пример))
print("=" * 60)
# 1. Базовая таблица сравнения
parallel_levels = [0.50, 0.75, 0.90, 0.95, 0.99]
core_list = [1, 2, 4, 8, 16, 32, 64, 128, 256]
print_comparison_table(parallel_levels, core_list)
# 2. Анализ предельной эффективности (на примере 75% параллелизуемости)
analyze_diminishing_returns(0.75, max_cores=64)
# 3. Симуляция конкретной задачи
print("\n" + "=" * 60)
print(" Симуляция реального сценария")
print("=" * 60)
# Сценарий 1: Рендеринг видео (высокопараллельный, 95%)
print("\n[Сцена 1] Задача рендеринга видео")
print(Последовательная часть: чтение файла, инициализация кодера = 10ms)
print(Параллельная часть: покадровый рендеринг = 200ms (одно ядро))
compare_serial_vs_parallel(10, 200, 16)
# Сценарий 2: Транзакция базы данных (умеренно параллелизуемая, 50%)
print("\n[Сценарий 2] Обработка транзакций базы данных")
print(" Последовательная часть: запись логов, управление блокировками = 50ms")
print(Параллельная часть: выполнение запросов = 50ms (одно ядро))
compare_serial_vs_parallel(50, 50, 16)
# Сценарий 3: Чистые вычисления (высокопараллельные, 99%)
print("\n[Сценарий 3] Научные вычисления / матричные операции)
print(Последовательная часть: агрегация результатов = 1ms)
print(Параллельная часть: умножение матриц = 1000ms (одно ядро))
compare_serial_vs_parallel(1, 1000, 16)
# 4. Рекомендуемое количество ядер
print("\n" + "=" * 60)
print(Практический совет: как определить, нужно ли вам больше ядер?)
print("=" * 60)
# Вычислить точку перегиба 'цена-производительность' при различных коэффициентах параллелизма
print(f" {'Коэффициент параллелизма':>10} | {'Ускорение 2 ядер':>10} | {'Ускорение 4 ядер':>10} | {'Ускорение 8 ядер':>10} | {'Ускорение 16 ядер':>9} | {'Рекомендуемое число ядер':>10}")
print("-" * 70)
for pf in [0.50, 0.60, 0.70, 0.75, 0.80, 0.85, 0.90, 0.95, 0.99]:
ratios = []
prev = 1.0
recommendations = []
for cores in [2, 4, 8, 16]:
sp = amdahl_speedup(pf, cores)
gain = (sp / prev - 1) * 100 # Процентное улучшение относительно предыдущего уровня
ratios.append(gain)
prev = sp
if gain < 10:
recommendations.append(cores)
# Рекомендация: улучшение менее чем на 10% не стоит апгрейда
suggested = ">=16 ядер" if not recommendations else f{recommendations[0]} ядер достаточно
print(f" {pf*100:>9.0f}% | {ratios[0]:>9.1f}% | {ratios[1]:>9.1f}% | {ratios[2]:>9.1f}% | {ratios[3]:>8.1f}% | {suggested:>10}")
print()
print(" Практическое правило:")
print(- Ежедневная офисная работа, просмотр веб-страниц (низкая доля параллелизма): достаточно 4-6 ядер)
print(- Компиляция программ, лёгкие игры: 6-8 ядер)
print(- Видеорендеринг, 3D-моделирование: 8-16 ядер)
print(- Научные вычисления, обучение ИИ: чем больше ядер, тем лучше (но также учитывайте видеопамять и пропускную способность памяти))
Запустив приведённый выше код, вы увидите несколько ключевых явлений:
- Доля параллелизма определяет всё: Задача, которая распараллеливается на 50%, даже с 256 ядрами даёт ускорение менее чем в 2 раза. Но задача, распараллеливаемая на 99%, может достичь почти 14-кратного ускорения на 16 ядрах.
- Убывающая предельная отдача: Наибольшее улучшение происходит при переходе с 1 ядра на 2 ядра; после этого каждый раз, когда количество ядер удваивается, дополнительное ускорение становится всё меньше и меньше.
- Снижение эффективности: Когда количество ядер удваивается, эффективность (фактическое ускорение / количество ядер) продолжает снижаться. Именно поэтому слепое наращивание ядер не окупается.
Интерактивная демонстрация: анимация распределения задач по нескольким ядрам
В следующей демонстрации 12 вычислительных задач назначаются1 ядруи4 ядрамдля обработки. В режиме 1 ядра задачи ставятся в очередь и выполняются последовательно; в режиме 4 ядер четыре задачи обрабатываются параллельно одновременно. Обратите внимание на разницу в скорости заполнения полосы выполнения и времени завершения между двумя режимами.
Демонстрация анимации распределения задач по нескольким ядрам (пример)
Очередь задач1 ядро (последовательная обработка)
4 ядра (параллельная обработка)
Многоядерность в реальных приложениях
Как операционная система планирует работу многоядерных систем
Планировщик в операционной системе отвечает за назначение различных процессов и потоков различным ядрам.
Будет учитывать:
- Балансировка нагрузки: стараться, чтобы каждое ядро было занято, избегая ситуации «одно ядро в беде, многие ядра смотрят».
- Кэш-аффинность: стараться, чтобы один и тот же поток выполнялся на одном и том же ядре, используя собственный кэш этого ядра.
- Управление энергопотреблением: при низкой нагрузке отключать некоторые ядра для экономии энергии.
Многоядерность против Hyper-Threading
Помимо физической многоядерности, существует также технология, называемаяHyper-Threading (SMT)。
Hyper-Threading заставляет одно физическое ядро выглядеть как два «логических ядра». Когда один поток ожидает данные (промах кэша), ядро может переключиться на другой поток, чтобы продолжить работу.
| Решение | Аппаратная стоимость | Прирост производительности | Применимые сценарии |
|---|---|---|---|
| Физическая многоядерность | Высокая (требуется дублирование всего исполнительного блока) | Почти вдвое (в идеальном случае) | Длительные задачи с высокой нагрузкой |
| Гиперпоточность (SMT) | Низкие (требуется только дополнительные регистры + логика управления). | Около 10%~30% | Полностью использовать время ожидания |
Многие современные процессоры используют обе технологии одновременно. Например, Intel Core i7 может быть "4 физических ядра + гиперпоточность = 8 логических ядер".
Итоги и проверка
Кратко: многоядерность заключается в размещении нескольких ядер CPU на одном чипе для параллельного выполнения задач. Но закон Амдала гласит, что реальное ускорение зависит от доли задачи, которая может быть распараллелена, — последовательная часть определяет верхний предел ускорения, и чем больше ядер, тем меньше предельная отдача.
Вопросы для самопроверки
- Задача на 80% может выполняться параллельно. Какое ускорение даёт закон Амдала на 4-ядерном CPU?
Ответ: S=0.2, P=0.8, N=4. Ускорение = 1/(0.2 + 0.8/4) = 1/(0.2 + 0.2) = 1/0.4 = 2.5x. Иначе говоря, 4 ядра дают всего 2.5-кратное ускорение, эффективность всего 62.5%.
- Если задача может быть распараллелена почти на 100%, каков теоретический верхний предел? Почему на практике он всё равно недостижим?
Ответ: Теоретический верхний предел бесконечен (ускорение = числу ядер). Но на практике он ограничен: (1) накладными расходами на разделение и объединение задач; (2) задержками синхронизации и связи между ядрами; (3) конкуренцией за общие ресурсы (например, пропускную способность памяти).
- Почему современные серверные процессоры могут иметь 64 или даже 128 ядер, а персональные компьютеры обычно только от 4 до 16 ядер?
Ответ: Запросы, обрабатываемые серверами (такие как веб-запросы и запросы к базам данных), естественно обладают высокой степенью параллелизма — каждый пользовательский запрос независим, поэтому доля распараллеливаемой части близка к 100%. С другой стороны, повседневные задачи на персональных компьютерах (например, отрисовка в браузере и обработка документов Office) имеют ограниченный параллелизм; слишком много ядер не принесёт пользы, а лишь увеличит энергопотребление и стоимость.