阿姆达尔定律可以用来评估通过让一个计算中的一部分并行处理的方式能够提升多少速度.阿姆达尔定律在1967年由Gene Amdal提出后得名.大部分从事并发编程的开放人员对并发带来的增速都有很强的直觉.即使在他们不知道阿姆达尔定律的情况下.但是仍然很有必要知道阿姆达尔定律.
我们会先用算术的方式然后再用图表的方式来讲解阿姆达尔定律.
阿姆达尔定律定义
一个程序或算法需要分成两部分来讨论并行处理:
- 不能被并行处理的部分
- 能够被并行处理的部分
想象一个处理磁盘文件的程序.有一小部分程序用来扫描文件夹,并在内存中创建一个文件列表.然后才是将每个文件交给一个线程处理.扫描目录和创建文件列表的部分不能被并行处理.而处理文件的这部分则可以.
顺序执行程序所需要的总时长我们称为T.时间T包含了不能并行处理和能够并行处理两部分的执行时间.不能并行处理的执行时长我们称为B.能够并行处理的执行时长我们称为T-B.以下列表中给出了具体的定义:
- T = 顺序执行所需要的总时长
- B = 不能够并行处理部分的执行时长
- T - B = 能够并行处理部分的执行时长(只要是顺序执行都不计入其中)
所以我们有了如下定义:
T = B + (T - B)
复制代码
T - B所表示的能够并行处理的部分,可以通过并行执行来提速.至于能够提升多少速度取决于使用了多少个线程来执行它.我们用N来表示线程数量.最终我们可以用以下等式来表示并行部分所需的执行时长:
(T - B) / N
复制代码
另一种写法:
(1/N) * (T - B)
复制代码
根据阿姆达尔定律,当通过N个线程来执行可并行处理部分后,程序的总执行时长可以表示为:
T(N) = B + ( T(1) - B ) / N
复制代码
这个等式仍然成立.
一个评估实例
为了更好的理解阿姆达尔定律,我们需要一个评估实例.我们将程序的总执行时长设为1.假设程序中不可并行处理部分占比40%,执行时长占比0.4,可并行处理部分占比60%,执行时长占比0.6.
假设程序的并行因子为2(有两个线程同时执行可并行处理部分, 所以N = 2)可得:
T(2) = 0.4 + ( 1 - 0.4 ) / 2
= 0.4 + 0.6 / 2
= 0.4 + 0.3
= 0.7
复制代码
如果将并行因子从2替换为5可得:
T(5) = 0.4 + ( 1 - 0.4 ) / 5
= 0.4 + 0.6 / 5
= 0.4 + 0.12
= 0.52
复制代码
图表讲解阿姆达尔定律
为了更好的理解阿姆达尔定律,我们尝试使用图表来说明.
首先,一个程序可以被分成可并行处理部分B和不可并行处理部分1 - B,如下图所示:
图中上方给出的比例尺可以用于表示程序顺序执行所需的总时长T(1).
当我们设置并行因子为2时,执行时长如下图所示:
当我们设置并行因子为3时,执行时长如下图所示:
优化算法
根据阿姆达尔定律我们可以很自然的得出结论,程序中可以被并行处理的部分可以通过增加硬件来提速.(增加更多的线程和cpu数量).然而程序中不能并行处理的部分只能通过优化代码来提速.因此,你可以通过优化不可并行处理部分的代码来提升运行速度和可并行性.你可以通过更改算法的方式来减小程序不可并行处理部分的占比,将一些计算移到并行处理部分内.
优化顺序执行部分
当你优化程序同步执行的部分时,同样可以使用阿姆达尔定律来评估程序优化后带来的运行增速.假设不可并行处理部分B的优化因子为O,那么阿姆达尔定律可以表示为:
T(O,N) = B / O + (1 - B / O) / N
复制代码
记得,不可并行处理部分现在用B / O来表示,因此可并行处理部分需要用 1 - B / O来表示.
设B为0.4, O为2, N为5, 可得出等式:
T(2,5) = 0.4 / 2 + (1 - 0.4 / 2) / 5
= 0.2 + (1 - 0.4 / 2) / 5
= 0.2 + (1 - 0.2) / 5
= 0.2 + 0.8 / 5
= 0.2 + 0.16
= 0.36
复制代码
执行时长 vs. 增速
我们不单单可以使用阿姆达尔定律来评估程序优化和并行处理后的执行时长.我们还可以使用阿姆达尔定律来评估增速,即得出新算法比旧的要快多少倍.
假设旧算法的执行时长为T,则增速可以表示为:
Speedup = T / T(O,N)
复制代码
我们经常设置T为1用分数的方式来表达新算法的执行时长比旧的要快多少倍.
等式可以表示为:
Speedup = 1 / T(O,N)
复制代码
如果将T(O,N)替换为阿姆达尔公式可得:
Speedup = 1 / ( B / O + (1 - B / O) / N )
复制代码
设B=0.4, O=2, N=5, 由公式可得:
Speedup = 1 / ( 0.4 / 2 + (1 - 0.4 / 2) / 5)
= 1 / ( 0.2 + (1 - 0.4 / 2) / 5)
= 1 / ( 0.2 + (1 - 0.2) / 5 )
= 1 / ( 0.2 + 0.8 / 5 )
= 1 / ( 0.2 + 0.16 )
= 1 / 0.36
= 2.77777 ...
复制代码
假设程序的不可并行处理部分的优化因子为2,可并行处理部分的优化因子为5,那么新的算法实现大约比旧的版本要快2.77777倍.
要多测量而不只是评估
虽然阿姆达尔定律可以帮助你从理论上来评估一个算法并行化后的增速幅度.但不要太过依赖这些评估.实践中,当你优化和并行化一个算法时还有许多其他的因素参与进来共同影响程序的执行速度.
内存,cpu缓存,磁盘和网卡的读写速度等(如果有使用到磁盘和网络的话)也会成为限制因素.如果一个设计为并行化的新算法执行过程中,对CPU缓存的命中率过低的情况下,也会达不到使用(T - B) x N的增速预期.同样的,如果共享内存区域,磁盘或网卡和网络链接时长处于过载的情况下,也会达不到预期的增速效果.
更好的做法是使用阿姆达尔定律来评估程序中哪些地方需要优化,而通过测量的手段来检查优化后带来的增速.需要谨记的一点是一个高效的串行算法可能要比一个并行算法还要优越的多.不只是因为串行执行中没有额外的协作开销(即需要分批处理任务再聚合处理所得结果),还因为单CPU算法可以更加适应底层硬件的优化(CPU传输通道, CPU缓存等).
该系列博文为笔者复习基础所著译文或理解后的产物,复习原文来自Jakob Jenkov所著Java Concurrency and Multithreading Tutorial