鍍金池/ 教程/ Java/ 阿姆達爾定律
Slipped Conditions
阻塞隊列
無阻塞算法
嵌套管程鎖死
Java 并發(fā)性和多線程介紹
死鎖
線程安全及不可變性
并發(fā)編程模型
Java 中的讀/寫鎖
剖析同步器
競態(tài)條件與臨界區(qū)
多線程的優(yōu)點
CAS
線程通信
如何創(chuàng)建并運行 java 線程
阿姆達爾定律
避免死鎖
信號量
多線程的代價
饑餓和公平
線程池
重入鎖死
Java 中的鎖
Java 內存模型
線程安全與共享資源
Java 同步塊

阿姆達爾定律

阿姆達爾定律可以用來計算處理器平行運算之后效率提升的能力。阿姆達爾定律因 Gene Amdal 在 1967 年提出這個定律而得名。絕大多數使用并行或并發(fā)系統(tǒng)的開發(fā)者有一種并發(fā)或并行可能會帶來提速的感覺,甚至不知道阿姆達爾定律。不管怎樣,了解阿姆達爾定律還是有用的。

我會首先以算術的方式介紹阿姆達爾定律定律,然后再用圖表演示一下。

阿姆達爾定律定義

一個程序(或者一個算法)可以按照是否可以被并行化分為下面兩個部分:

  • 可以被并行化的部分
  • 不可以被并行化的部分

假設一個程序處理磁盤上的文件。這個程序的一小部分用來掃描路徑和在內存中創(chuàng)建文件目錄。做完這些后,每個文件交個一個單獨的線程去處理。掃描路徑和創(chuàng)建文件目錄的部分不可以被并行化,不過處理文件的過程可以。

程序串行(非并行)執(zhí)行的總時間我們記為 T。時間 T 包括不可以被并行和可以被并行部分的時間。不可以被并行的部分我們記為 B。那么可以被并行的部分就是 T-B。下面的列表總結了這些定義:

  • T = 串行執(zhí)行的總時間
  • B = 不可以并行的總時間
  • T- B = 并行部分的總時間

從上面可以得出:

T = B + (T – B)

首先,這個看起來可能有一點奇怪,程序的可并行部分在上面這個公式中并沒有自己的標識。然而,由于這個公式中可并行可以用總時間 T 和 B(不可并行部分)表示出來,這個公式實際上已經從概念上得到了簡化,也即是指以這種方式減少了變量的個數。

T- B 是可并行化的部分,以并行的方式執(zhí)行可以提高程序的運行速度。可以提速多少取決于有多少線程或者多少個 CPU 來執(zhí)行。線程或者 CPU 的個數我們記為 N。可并行化部分被執(zhí)行的最快時間可以通過下面的公式計算出來:

(T – B ) / N

或者通過這種方式

(1 / N) * (T – B)

維基中使用的是第二種方式。

根據阿姆達爾定律,當一個程序的可并行部分使用 N 個線程或 CPU 執(zhí)行時,執(zhí)行的總時間為:

T(N) = B + ( T – B ) / N

T(N)指的是在并行因子為 N 時的總執(zhí)行時間。因此,T(1)就執(zhí)行在并行因子為 1 時程序的總執(zhí)行時間。使用 T(1)代替 T,阿姆達爾定律定律看起來像這樣:

T(N) = B + (T(1) – B) / N

表達的意思都是是一樣的。

一個計算例子

為了更好的理解阿姆達爾定律,讓我們來看一個計算的例子。執(zhí)行一個程序的總時間設為 1。程序的不可并行化占 40%,按總時間 1 計算,就是 0.4,可并行部分就是 1–0.4=0.6.

在并行因子為 2 的情況下,程序的執(zhí)行時間將會是:

T(2) = 0.4 + ( 1 - 0.4 ) / 2
 = 0.4 + 0.6 / 2
 = 0.4 + 0.3
 = 0.7

在并行因子為 5 的情況下,程序的執(zhí)行時間將會是:

T(5) = 0.4 + ( 1 - 0.4 ) / 5
 = 0.4 + 0.6 / 6
 = 0.4 + 0.12
 = 0.52

阿姆達爾定律圖示

為了更好地理解阿姆達爾定律,我會嘗試演示這個定定律是如何誕生的。

首先,一個程序可以被分割為兩部分,一部分為不可并行部分 B,一部分為可并行部分 1–B。如下圖:

http://wiki.jikexueyuan.com/project/java-concurrent/images/24.png" alt="" />

在頂部被帶有分割線的那條直線代表總時間 T(1)。

下面你可以看到在并行因子為 2 的情況下的執(zhí)行時間:

http://wiki.jikexueyuan.com/project/java-concurrent/images/25.png" alt="" />

并行因子為 3 的情況:

http://wiki.jikexueyuan.com/project/java-concurrent/images/26.png" alt="" />

優(yōu)化算法

從阿姆達爾定律可以看出,程序的可并行化部分可以通過使用更多的硬件(更多的線程或 CPU)運行更快。對于不可并行化的部分,只能通過優(yōu)化代碼來達到提速的目的。因此,你可以通過優(yōu)化不可并行化部分來提高你的程序的運行速度和并行能力。你可以對不可并行化在算法上做一點改動,如果有可能,你也可以把一些移到可并行化放的部分。

優(yōu)化串行分量

如果你優(yōu)化一個程序的串行化部分,你也可以使用阿姆達爾定律來計算程序優(yōu)化后的執(zhí)行時間。如果不可并行部分通過一個因子 O 來優(yōu)化,那么阿姆達爾定律看起來就像這樣:

T(O, N) = B / O + (1 - B / O) / N

記住,現(xiàn)在程序的不可并行化部分占了 B / O 的時間,所以,可并行化部分就占了 1 - B / O 的時間。

如果 B 為 0.1,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. 加速

到目前為止,我們只用阿姆達爾定律計算了一個程序或算法在優(yōu)化后或者并行化后的執(zhí)行時間。我們也可以使用阿姆達爾定律計算加速比(speedup),也就是經過優(yōu)化后或者串行化后的程序或算法比原來快了多少。

如果舊版本的程序或算法的執(zhí)行時間為 T,那么增速比就是:

Speedup = T / T(O , N);

為了計算執(zhí)行時間,我們常常把 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 來優(yōu)化不可并行化部分,一個因子 5 來并行化可并行化部分,這個程序或算法的最新優(yōu)化版本最多可以比原來的版本快 2.77777 倍。

測量,不要僅是計算

雖然阿姆達爾定律允許你并行化一個算法的理論加速比,但是不要過度依賴這樣的計算。在實際場景中,當你優(yōu)化或并行化一個算法時,可以有很多的因子可以被考慮進來。

內存的速度,CPU 緩存,磁盤,網卡等可能都是一個限制因子。如果一個算法的最新版本是并行化的,但是導致了大量的 CPU 緩存浪費,你可能不會再使用 xN 個 CPU 來獲得 xN 的期望加速。如果你的內存總線(memory bus),磁盤,網卡或者網絡連接都處于高負載狀態(tài),也是一樣的情況。

我們的建議是,使用阿姆達爾定律定律來指導我們優(yōu)化程序,而不是用來測量優(yōu)化帶來的實際加速比。記住,有時候一個高度串行化的算法勝過一個并行化的算法,因為串行化版本不需要進行協(xié)調管理(上下文切換),而且一個單個的 CPU 在底層硬件工作(CPU 管道、CPU 緩存等)上的一致性可能更好。

上一篇:Java 同步塊下一篇:線程通信