1. 双核变四核,为什么没快 4 倍

老板说"加机器就能快",于是核心数从 1 涨到 4、到 64。可你一测:1 核跑 100 秒的任务,4 核跑了 30 秒——不是 25 秒;64 核竟然还有 20 秒。钱花出去了,倍数却越来越"不划算"。

这不是编译器偷懒,而是一条冷冰冰的物理/数学上限在起作用——Amdahl 定律。它回答的是:一个程序里只有一部分能并行,整体到底最多能快多少?

本质一句话:程序里跑不并行的那一块,决定了它再怎么加核也快不到哪去。

2. 加速比:先定义"快了多少"

记:

  • 原串行总耗时 T₁(1 个核心跑完)。
  • 并行化比例 P(0~1):原本可以并行执行的那部分时间占比。
  • 串行比例 1 − P:怎么都并行不了的硬骨头(初始化、I/O、临界区、依赖链)。
  • N 个核心跑,并行部分耗时按 N 等分缩成 P·T₁ / N,串行部分纹丝不动,仍是 (1 − P)·T₁

于是 N 核总耗时:

1
2
T(N) = (1 − P)·T₁  +  P·T₁ / N
= T₁ · [ (1 − P) + P / N ]

加速比(speedup) 定义为"省了多少倍时间":

1
S(N) = T₁ / T(N) = 1 / ( (1 − P) + P / N )

这就是 Amdahl 定律的标准形式。N → ∞ 时分母里 P/N → 0,加速比被串行部分钉死:

1
S_max = lim S(N) = 1 / (1 − P)

只要还有 5% 的代码跑不了并行,你再堆 1000 个核,整体最快也只能快 20 倍。 这就是定律最扎心的结论。

3. 一张图看收敛(数据真实可复算)

下面这张曲线,是把上面公式按不同 P 画出来:X 轴是核数 N(对数刻度),Y 轴是加速比。注意蓝线(P=50%)几乎贴着地面——一半代码跑不了并行,加核基本白加;红线(P=99%)起初猛涨,但到 128 核也才 ~56 倍,离 128 差得远。

Amdahl 定律:加速比随核数 N 的收敛(Y 轴 = 加速比,X 轴 = 核数,对数) 0 10 20 30 40 50 60 1 2 4 8 16 32 64 128 核数 N(2 的幂) P=50% P=90% P=95% P=99%

同口径的数值表(行=并行比例 P,列=核数 N;右上角是串行部分钉死的上限 1/(1−P)):

P \ N 1 2 4 8 16 32 64 128
50% 1.00 1.33 1.60 1.78 1.88 1.94 1.97 1.98
90% 1.00 1.82 3.08 4.71 6.40 7.80 8.77 9.34
95% 1.00 1.90 3.48 5.93 9.14 12.55 15.42 17.41
99% 1.00 1.98 3.88 7.48 13.91 24.43 39.26 56.39

读这张表的三条直觉:

  • P 决定天花板:P=50% 再怎么加核也到不了 2 倍;P=99% 理论上限是 100 倍。
  • 越往后越不划算:从 32 核翻到 64 核,P=99% 只从 24.4 涨到 39.3(多了 15),而 1→2 核直接 1→1.98。并行收益是边际递减的。
  • N 很大时曲线变平:因为分母里 P/N 已经小到可以忽略,剩下全是 (1−P) 在说了算。

4. 可运行代码:自己复算这张表

下面这段 Python 把公式原样落地,跑出来就是上表的数字。你可以改 PN 任意组合:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/env python3
# amdahl.py —— 复算 Amdahl 加速比表
# 运行:python3 amdahl.py

def speedup(p, n):
"""Amdahl 加速比:S = 1 / ((1-p) + p/n)"""
return 1.0 / ((1.0 - p) + p / n)

def main():
print("S = 1 / ((1-P) + P/N) (行=并行比例 P,列=核数 N)")
print(" P \\ N | 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128")
for p in (0.50, 0.90, 0.95, 0.99):
cells = ["%5.2f" % speedup(p, n) for n in (1, 2, 4, 8, 16, 32, 64, 128)]
print(" %.2f | %s" % (p, " | ".join(cells)))
print("\n串行部分钉死的上限 S_max = 1/(1-P):")
for p in (0.50, 0.90, 0.95, 0.99):
print(" P=%.2f -> 最快只能快 %.2f 倍" % (p, 1.0 / (1.0 - p)))

if __name__ == "__main__":
main()

真实输出:

1
2
3
4
5
6
7
8
9
10
11
12
S = 1 / ((1-P) + P/N)    (行=并行比例 P,列=核数 N)
P \ N | 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128
0.50 | 1.00 | 1.33 | 1.60 | 1.78 | 1.88 | 1.94 | 1.97 | 1.98
0.90 | 1.00 | 1.82 | 3.08 | 4.71 | 6.40 | 7.80 | 8.77 | 9.34
0.95 | 1.00 | 1.90 | 3.48 | 5.93 | 9.14 | 12.55 | 15.42 | 17.41
0.99 | 1.00 | 1.98 | 3.88 | 7.48 | 13.91 | 24.43 | 39.26 | 56.39

串行部分钉死的上限 S_max = 1/(1-P):
P=0.50 -> 最快只能快 2.00 倍
P=0.90 -> 最快只能快 10.00 倍
P=0.95 -> 最快只能快 20.00 倍
P=0.99 -> 最快只能快 100.00 倍

5. 时间拆分:为什么串行部分"纹丝不动"

把一次运行的总时间画成一条横条——红色是死活并行不了的串行段,蓝色是能并行的段。加核时,红色那段的长度一点儿没变,只是蓝色被越切越薄:

P=90%:加核只压缩蓝色并行段,红色串行段恒定 1 核 10% 并行 90% 16 核 10% 并行段被压到 5.6%,其余被串行段钉死 串行(不可压缩) 并行

看懂这张图,定律就不需要背了:加速比的瓶颈不是"你并了多少",而是"你还剩多少没法并"

6. 反例与边界:Amdahl 不是唯一真相

Amdahl 有一个隐含前提——问题规模固定(总工作量不变,只是把它分掉)。可现实中常常是反过来:核多了,你就想算更大的问题。这就引出了它的"对偶"——Gustafson 定律(弱缩放)

Gustafson 说:若随核心数 N问题规模同比放大,并行部分的时间也随之放大,串行部分占比被稀释。其缩放加速比为:

1
S_weak(N) = N − (1 − N)·(1 − P) = P·N + (1 − P)

P=0.95N=128 时,Amdahl(强缩放)只给 17.4 倍,而 Gustafson(弱缩放)给 0.95×128 + 0.05 = 121.65 倍——几乎线性。这不是定律打架,而是两个不同假设

假设 Amdahl(强缩放) Gustafson(弱缩放)
问题规模 固定 随核数放大
问的是 同一任务最快多快 同样时间能算多大
上限 被串行部分钉死 1/(1−P) 趋近 N(线性)
典型场景 跑完一个已知任务省时间 天气预报、渲染,吃满算力算更细

所以别被"Amdahl 说并行没用"误导——它只在"任务大小不变"时成立。工程上两条都要看:想省时间看 Amdahl,想扩规模看 Gustafson。

7. 工程启示:先量,再并行

Amdahl 给开发者的最实用忠告,不是公式,而是方法论

  1. 先 profile,再动手。没量化 P 就盲目 parallel_for,八成加速比难看。串行瓶颈常藏在 I/O、锁、初始化、依赖链里,肉眼看不出。
  2. 优化串行部分收益最高。把 1−P 从 10% 压到 5%,上限从 10 倍跳到 20 倍——这比再买一倍机器划算。
  3. 并行收益边际递减,别无脑堆核。N 从 16→32 的边际加速,远低于 1→2。超出拐点后,加核的性价比骤降。
  4. 区分强/弱缩放定目标。要"省时间"就盯 Amdahl;要"算更大"就按 Gustafson 规划规模。

C++ / Java 对照:两个世界的并行原语都逃不开这条定律。Java 的 parallelStream() 看着一行就并行,但若中间有 synchronized 块或共享 mutable 状态,那部分就是 1−P;C++ 的 std::for_each(par, ...) 同理,临界区、false sharing 都会把 P 压低。定律不挑语言——它量的是"能真并行的时间占比"这个客观事实。

8. 对比表与小结

维度 结论 一句话
加速比公式 S = 1 / ((1−P) + P/N) 串行项 + 并行项被 N 除
极限上限 S_max = 1/(1−P) 5% 串行 → 最多 20 倍
收益曲线 边际递减 越往后加核越不划算
对偶定律 Gustafson 弱缩放 S≈N 问题放大时近线性
工程顺序 P → 压 1−P → 再并行 别无脑堆核

🐾 小结:

  • Amdahl 定律:S(N) = 1 / ((1−P) + P/N),上限 1/(1−P)。跑不掉并行的那块,就是整体速度的天花板。
  • 加速比随 N 边际递减;想"省时间"看强缩放 Amdahl,想"算更大"看弱缩放 Gustafson,两者假设不同、不矛盾。
  • 工程上先 profile 量化 P,优先压缩串行部分 1−P,再考虑并行与加核——盲目 parallel 只是把蓝色段切开,红色段纹丝不动。
  • 定律与语言无关:Java parallelStream、C++ 并行算法,只要存在锁 / 共享状态 / I/O,那部分就是 1−P

相关阅读:上一篇《硬件、软件接口》讲"CPU 怎么访问设备",本篇讲"多核下整体能快多少"——一个是横向的接口边界,一个是纵向的性能边界。