现在位置: 首页 > 计算机组成原理 > 正文

多核心是怎么回事

当单核 CPU 的性能提升遇到瓶颈(功耗墙、频率墙、流水线复杂度墙)后,工程师想出了一个「简单粗暴」但有效的办法:在一个芯片上放多个 CPU 核心。本讲将介绍多核心的原理、优势以及阿姆达尔定律对并行加速的限制。


生活中的类比:快餐店厨房

想象一家快餐店在午餐高峰期的情况。

单核模式:只有一个厨师。他在拼命加快速度(提高频率),优化做菜流程(流水线),但再怎么优化,同时只能做一道菜。客人排长队,等得不耐烦。

多核模式:老板雇了 4 个厨师,一人一个灶台。4 个厨师可以同时做 4 道不同的菜。不需要每个厨师变得更快,但整体的出餐量翻了好几倍。

这就是多核心的设计哲学:当单个核心很难再提速时,不如多放几个核心,让它们并行工作。

多核心不等于「把所有东西都做 4 份」。4 个核心通常共享同一块内存、同一个硬盘接口和同一个操作系统,它们只是在「计算单元」层面做了复制。就像 4 个厨师共享同一个冰箱和食材仓库。


单核 vs 多核:架构对比

单核 CPU 的困境

2000 年代初期,CPU 行业靠不断提高频率来提升性能——从 1 GHz 到 2 GHz 再到 3 GHz。但这条路很快走到了尽头。

单核频率的三大瓶颈:

  • 功耗墙:频率越高,功耗指数级增长,散热成为物理极限
  • 内存墙:CPU 太快,内存跟不上,大部分时间在空等数据
  • ILP 墙:指令级并行(ILP)已挖掘到极限,单条指令流中可并行的指令越来越少

2005 年前后,Intel 和 AMD 先后放弃了单纯的「频率竞赛」,转向了多核心路线。

多核 CPU 的结构

一个典型的多核 CPU 内部结构如下:

  +-----------------------------------------------------------+
  |                         CPU 芯片                          |
  |  +-------------+  +-------------+  +-------------+  +-----+
  |  |   核心 1    |  |   核心 2    |  |   核心 3    |  |核 4 |
  |  | L1 Cache   |  | L1 Cache   |  | L1 Cache   |  | ... |
  |  | L2 Cache   |  | L2 Cache   |  | L2 Cache   |  |     |
  |  +------+------+  +------+------+  +------+------+  +--+--+
  |         |                |                |              |
  |         +----------------+----------------+--------------+
  |                          |
  |                    +-----+------+
  |                    | 共享 L3 Cache |
  |                    +-----+------+
  |                          |
  |                    +-----+------+
  |                    | 内存控制器   | ← 连接主内存 (RAM)
  |                    +------------+
  +-----------------------------------------------------------+

关键设计:

  • 每个核心有自己的 L1 和 L2 缓存(私有)
  • 多个核心共享一个 L3 缓存
  • 所有核心通过内存控制器访问同一块主内存
  • 操作系统负责将任务分配到不同的核心上

阿姆达尔定律:多核加速的天花板

「核心越多越快」这个直觉并不总是对的。1967 年,计算机科学家 Gene Amdahl 提出了一个著名的定律。

阿姆达尔定律:一个任务的整体加速比,受限于任务中必须串行执行的部分。

公式如下:

加速比 = 1 / (S + P/N)

其中:
  S = 任务中必须串行执行的比例(0~1)
  P = 任务中可以并行执行的比例(0~1),且 S + P = 1
  N = 并行处理单元的数量(核心数)

这个公式揭示了一个残酷的事实:

即使有无限多个核心,如果任务只有 50% 可以并行,加速比最多也只能到 2 倍。

推导:当 N 趋近于无穷大时,P/N 趋近于 0,加速比趋近于 1/S。

可并行比例1 核2 核4 核8 核16 核无穷多核(上限)
50%1.00x1.33x1.60x1.78x1.88x2.00x
75%1.00x1.60x2.29x2.91x3.37x4.00x
90%1.00x1.82x3.08x4.71x6.40x10.00x
95%1.00x1.90x3.48x5.93x9.14x20.00x
99%1.00x1.98x3.88x7.48x13.91x100.00x

观察这张表的规律:

  • 可并行比例越高,多核加速越明显。99% 可并行的任务在 16 核上能加速近 14 倍
  • 核心越多,边际收益越小。从 2 核到 4 核的加速比提升远大于从 16 核到 32 核
  • 串行部分是硬上限。50% 可并行时,即使用一万个核心,最多也只能快 2 倍

阿姆达尔定律解释了为什么「核心多不代表一定快」。如果你的程序主要是串行逻辑——比如一个计算步骤必须等待上一个步骤的结果——那么再多的核心也只能围观,帮不上忙。


交互演示:阿姆达尔定律计算器

下面的 Python 代码计算并打印阿姆达尔定律的加速比对比表格,同时绘制加速比随核心数变化的趋势,让你直观感受「串行瓶颈」的影响。

实例

"""
阿姆达尔定律计算器 (runoob 演示)
计算不同并行比例和核心数下的加速比
打印对比表格和趋势分析
"""


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("  阿姆达尔定律计算器 (runoob)")
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("  - 科学计算、AI 训练: 核心多多益善(但还需考虑显存、内存带宽)")

运行上面的代码,你会看到几个关键现象:

  • 并行比例决定一切:50% 可并行的任务,即使用 256 核,加速比也只有不到 2 倍。而 99% 可并行的任务在 16 核上就能加速近 14 倍。
  • 边际收益递减:从 1 核到 2 核的提升最大,之后每次翻倍核心数,带来的额外加速越来越小。
  • 效率下降:核心数翻倍,效率(实际加速比 / 核心数)持续下降。这就是为什么盲目堆核心数并不划算。

交互演示:多核任务分配动画

下面演示将 12 个计算任务分别分配给 1 个核心4 个核心 处理。1 核心模式下任务串行排队执行,4 核心模式下 4 个任务同时并行处理。观察两种模式下进度条的填充速度和完成时间差异。

多核任务分配动画演示 (runoob)

待处理任务队列(共 12 个)

1 核心(串行处理)

核心 1 进度

4 核心(并行处理)

核心 1 进度
核心 2 进度
核心 3 进度
核心 4 进度
1 核心耗时
0.0 s
4 核心耗时
0.0 s
加速比

多核心在现实中的应用

操作系统如何调度多核

操作系统中的调度器(Scheduler)负责将不同的进程和线程分配到不同的核心上。

它会考虑:

  • 负载均衡:尽量让每个核心都忙碌,避免「一核有难、多核围观」
  • 缓存亲和性:尽量让同一个线程在同一个核心上运行,利用该核心的私有缓存
  • 功耗管理:在负载低时关闭部分核心,节省电量

多核心 vs 超线程

除了物理多核,还有一种技术叫超线程(Hyper-Threading / SMT)

超线程让一个物理核心看起来像两个「逻辑核心」。当一个线程在等待数据(缓存未命中)时,核心可以切换到另一个线程继续工作。

方案硬件成本性能提升适用场景
物理多核高(需要复制整套执行单元)接近翻倍(理想情况)持续高负载任务
超线程 (SMT)低(只需额外寄存器+控制逻辑)约 10%~30%充分利用等待时间

很多现代 CPU 同时使用两种技术。比如 Intel Core i7 可能是「4 物理核 + 超线程 = 8 逻辑核」。


小结与检验

一句话总结:多核心通过在单个芯片上放置多个 CPU 核心来并行执行不同任务。但阿姆达尔定律指出,实际加速比取决于任务中可并行的比例——串行部分决定了加速上限,核心越多,边际收益越小。

自测题

  1. 一个任务有 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%。

  2. 如果一个任务几乎 100% 都可以并行执行,理论上限是多少?为什么现实中还是达不到?

    答案:理论上限是无穷大(加速比 = 核心数)。但现实中受限于:(1) 任务拆分和汇总有开销;(2) 核心间同步和通信有延迟;(3) 共享资源(如内存带宽)的竞争。

  3. 为什么现代服务器 CPU 可以有 64 甚至 128 个核心,但个人电脑通常只有 4~16 个核心?

    答案:服务器处理的请求(如网页请求、数据库查询)天然高度并行——每个用户请求相互独立,可并行比例接近 100%。而个人电脑的日常任务(如浏览器渲染、Office 文档处理)并行度有限,太多核心用不上,反而增加功耗和成本。