开始前,会这些就够了
- 理解比例和简单除法;可选代码使用 Python 3.10 或更新版本
四个人搬书,八个人接着搬,听上去应该只用一半时间。但如果每次开库房、核对唯一的清单都得按顺序完成,加人只能分担搬运,不能把所有步骤同时缩短。
程序也可能有这样的分工。我们先不比较任何真实 CPU,只给一份固定任务画时间账:单核原本用一百秒,其中哪些时间可以分给更多核心?
多找几个帮手,哪部分仍得排队做?
固定同一份工作:一个核心原本用 100 秒。假设各核心能力相同,可并行部分能均分给它们,暂时不计通信、调度和等待开销。串行比例按原来的 100 秒计算。
理想用时:40.00 秒,是原来的 2.50 倍速度。
20 + 80 ÷ 4 = 40.00 秒
继续增加核心时,这份模型的加速比上限约为 5.00 倍;这是上限,并不表示当前已经达到。
每条横条都用原来的 100 秒作同一把尺:蓝色是必须顺序完成的工作,绿色是分摊后的可并行工作。下面也列出数字,不必只靠颜色判断。
- 1 个核心100.00 秒
串行 20.00 秒 + 并行 80.00 秒
- 2 个核心60.00 秒
串行 20.00 秒 + 并行 40.00 秒
- 4 个核心 · 当前40.00 秒
串行 20.00 秒 + 并行 20.00 秒
- 8 个核心30.00 秒
串行 20.00 秒 + 并行 10.00 秒
- 16 个核心25.00 秒
串行 20.00 秒 + 并行 5.00 秒
- 32 个核心22.50 秒
串行 20.00 秒 + 并行 2.50 秒
这里只算固定工作量的理想加速上界,没有测过任何 CPU。任务规模改变、内存带宽不足或通信开销增加,都可能让真实结果与这里不同;核心数也不等于可以直接照搬的线程数设置。
先固定工作量,再决定哪一段可以一起做
假设这份工作在一个核心上,必须按顺序完成的部分用二十秒,其余八十秒可以平均分给多个核心。二十秒占原来总耗时的 20%,这个比例叫串行比例。它是时间占比,不是“20% 的代码行”。
这里还做了很强的假设:每个核心能力相同,分工完全均匀,没有安排任务、传递数据或互相等待的额外时间。核心数 N 指能实际参与这份工作的相同计算资源,不能直接把屏幕上显示的逻辑线程数量当成同等新增能力。
加速的只是可分工部分
四个核心下,八十秒的可分工部分理想地缩为二十秒,原来的二十秒串行工作仍在,总共四十秒。八个核心下,两部分合起来是三十秒。所以从四核加到八核,这次并没有把四十秒减为二十秒。
把单核时间记作 T1,串行比例记作 s,就得到 T(N) = T1 × [s + (1-s)/N]。加速倍数是 T1/T(N),比较的始终是同一份工作。这个简单的时间分账就是 Amdahl 定律的常见用法。
用程序算假设,不把算式装成跑分
代码只计算上面的公式,不启动多个工作线程,也不测机器耗时。Fraction 用分数保存 1/5,最后打印时才保留三位小数。cores 是模型输入,改成十六不会让电脑真的使用十六个核心。
当串行比例大于零,增加核心只能让可分工部分越来越接近零,总时间仍被串行部分挡住。在本模型中,核心数趋于无限时,加速倍数趋于 1/s;有限核心一般还达不到这个极限。
from fractions import Fraction
single_core_seconds = Fraction(100)
serial_fraction = Fraction(1, 5)
def model_seconds(total, serial, cores):
if total <= 0 or not 0 <= serial <= 1:
raise ValueError("positive total and serial fraction in [0, 1] required")
if type(cores) is not int or cores < 1:
raise ValueError("cores must be a positive integer")
return total * (serial + (1 - serial) / cores)
for cores in [1, 2, 4, 8, 16]:
seconds = model_seconds(single_core_seconds, serial_fraction, cores)
speedup = single_core_seconds / seconds
print(f"cores={cores:2d} time={float(seconds):7.3f} s speedup={float(speedup):.3f}x")
if serial_fraction > 0:
print(f"infinite-core limit={float(1 / serial_fraction):.3f}x")
else:
print("no finite serial-part limit in this model")怎样运行:保存为 amdahl_model.py,运行 python amdahl_model.py。使用 Python 3.10 或更新版本,仅用标准库;它计算虚构时间,不是 CPU 性能测试。
运行后对照结果
cores= 1 time=100.000 s speedup=1.000x
cores= 2 time= 60.000 s speedup=1.667x
cores= 4 time= 40.000 s speedup=2.500x
cores= 8 time= 30.000 s speedup=3.333x
cores=16 time= 25.000 s speedup=4.000x
infinite-core limit=5.000x真程序里,还要找到等待和资源限制
真实程序可能要创建任务、合并结果,还会争用内存或等待别的线程。更多核心可能让分工更快,也可能增加这些开销。评估时先用相同输入、相同结果要求测各段时间,再说明硬件和运行条件;不要用代码行数猜串行比例。
公式的上界依赖这里的假设,不是跨所有机器与算法的永恒天花板。换算法、改每核速度或增大工作量,都改变了问题。若目标是同样时间处理更多数据,要另外说明扩大的任务;若想让原任务更快,也可以先寻找串行部分有没有办法减少。
记住这三点
- 串行比例按原始单核耗时计算,不按代码行数计算。
- 固定工作量下,只有可并行部分能在理想模型中除以核心数。
- Amdahl 上界依赖同等核心、固定分工等假设,真实开销和性能仍需测量。
别把例子用过头
- 本例没有并行执行任务,输出是公式计算,不是这台电脑的性能成绩。
- 串行比例为零时,有限 N 的理想加速仍只有 N 倍;不存在由非零串行部分给出的固定极限。改变工作量或算法后,也不能原样沿用旧比例。
自己试一小步
怎样给“换更多核心就会快很多”这句话补上可检查的条件?
- 自选一份固定工作,画出必须先后完成与能分工处理的两段时间。
- 分别试两个串行比例和两组翻倍核心数,先预测趋势再运行模型。
- 另列至少两项真实程序可能增加的开销,写出你准备怎样单独观察它们。
做到这些,就算完成
- 是否用同一份工作、同一份单核时间作比较?
- 是否把每次新增核心的收益与相对单核的总加速分开?
- 是否标明公式没有测量哪些真实开销,而非把模型数字当作购买建议?
查原始资料
想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。
- NVIDIA:Strong Scaling and Amdahl’s Law (新标签页)↗
给出固定问题规模下的 Amdahl 公式、单核耗时中可并行部分的含义,以及核心数增长后的上界。
- LLNL:Introduction to Parallel Computing Tutorial (新标签页)↗
支持串行与并行时间分账、强扩展固定工作量,以及任务创建、同步、数据通信和资源限制带来的额外开销。
找一份资料继续练
先选一项,按下面的范围继续学;不必从头读完整门课程。
Oracle JDK 21 官方:JFR 性能诊断与并发契约 (新标签页)↗
已有可运行 Java 程序时,只录制一次代表性任务,分开找计算、等待和锁竞争,不凭线程数量预测加速倍数。
查看学习准备与访问说明 →