0
雷鋒網(wǎng) AI 科技評(píng)論按,本文是工程師 Jim Anderson 分享的關(guān)于「通過(guò)并發(fā)性加快 python 程序的速度」的文章的第三部分,主要內(nèi)容是 CPU 綁定程序加速相關(guān)。
在前面兩篇中,我們已經(jīng)講過(guò)了相關(guān)的概念以及 I/O 綁定程序的加速,這篇是這一系列文章的最后一篇,講的是 CPU 程序加速。雷鋒網(wǎng) AI 科技評(píng)論編譯整理如下:
如何加速 CPU 綁定程序
到目前為止,前面的例子都處理了一個(gè) I/O 綁定問(wèn)題?,F(xiàn)在,你將研究 CPU 綁定的問(wèn)題。如你所見(jiàn),I/O 綁定的問(wèn)題大部分時(shí)間都在等待外部操作(如網(wǎng)絡(luò)調(diào)用)完成。另一方面,CPU 限制的問(wèn)題只執(zhí)行很少的 I/O 操作,它的總體執(zhí)行時(shí)間取決于它處理所需數(shù)據(jù)的速度。
在我們的示例中,我們將使用一個(gè)有點(diǎn)愚蠢的函數(shù)來(lái)創(chuàng)建一些需要在 CPU 上運(yùn)行很長(zhǎng)時(shí)間的東西。此函數(shù)計(jì)算從 0 到傳入值的每個(gè)數(shù)字的平方和:
你將處理一大批數(shù)據(jù),所以這需要一段時(shí)間。記住,這只是代碼的一個(gè)占位符,它實(shí)際上做了一些有用的事情,需要大量的處理時(shí)間,例如計(jì)算公式的根或?qū)Υ笮蛿?shù)據(jù)結(jié)構(gòu)進(jìn)行排序。
CPU 綁定的同步版本
現(xiàn)在讓我們看一下這個(gè)示例的非并發(fā)版本:
import time
def cpu_bound(number):
return sum(i * i for i in range(number))
def find_sums(numbers):
for number in numbers:
cpu_bound(number)
if __name__ == "__main__":
numbers = [5_000_000 + x for x in range(20)]
start_time = time.time()
find_sums(numbers)
duration = time.time() - start_time
print(f"Duration {duration} seconds")
此代碼調(diào)用 cpu_bound() 20 次,每次使用不同的大數(shù)字。它在單個(gè) CPU 上單個(gè)進(jìn)程中的單個(gè)線程上完成所有這些工作。執(zhí)行時(shí)序圖如下:
與 I/O 綁定示例不同,CPU 綁定示例的運(yùn)行時(shí)間通常相當(dāng)一致。這臺(tái)機(jī)器大約需要 7.8 秒:
顯然我們可以做得更好。這都是在沒(méi)有并發(fā)性的單個(gè) CPU 上運(yùn)行的。讓我們看看我們能做些什么來(lái)改善它。
線程和異步版本
你認(rèn)為使用線程或異步重寫(xiě)此代碼會(huì)加快速度嗎?
如果你回答「一點(diǎn)也不」,這是有道理的。如果你回答,「它會(huì)減慢速度,」那就更對(duì)啦。
原因如下:在上面的 I/O 綁定示例中,大部分時(shí)間都花在等待緩慢的操作完成上。線程和異步通過(guò)允許你重疊等待的時(shí)間而不是按順序執(zhí)行,這能加快速度。
但是,在 CPU 綁定的問(wèn)題上,不需要等待。CPU 會(huì)盡可能快速地啟動(dòng)以解決問(wèn)題。在 python 中,線程和任務(wù)都在同一進(jìn)程中的同一個(gè) CPU 上運(yùn)行。這意味著一個(gè) CPU 不僅做了非并發(fā)代碼的所有工作,還需要做線程或任務(wù)的額外工作。它花費(fèi)的時(shí)間超過(guò) 10 秒:
我已經(jīng)編寫(xiě)了這個(gè)代碼的線程版本,并將它與其他示例代碼放在 Github repo 中,這樣你就可以自己測(cè)試它了。
CPU 綁定的多處理版本
現(xiàn)在,你終于要接觸多處理真正與眾不同的地方啦。與其他并發(fā)庫(kù)不同,多處理被顯式設(shè)計(jì)為跨多個(gè) CPU 共同承擔(dān)工作負(fù)載。它的執(zhí)行時(shí)序圖如下所示:
它的代碼是這樣的:
import multiprocessing
import time
def cpu_bound(number):
return sum(i * i for i in range(number))
def find_sums(numbers):
with multiprocessing.Pool() as pool:
pool.map(cpu_bound, numbers)
if __name__ == "__main__":
numbers = [5_000_000 + x for x in range(20)]
start_time = time.time()
find_sums(numbers)
duration = time.time() - start_time
print(f"Duration {duration} seconds")
這些代碼和非并發(fā)版本相比幾乎沒(méi)有要更改的。你必須導(dǎo)入多處理,然后把數(shù)字循環(huán)改為創(chuàng)建多處理.pool 對(duì)象,并使用其.map()方法在工作進(jìn)程空閑時(shí)將單個(gè)數(shù)字發(fā)送給它們。
這正是你為 I/O 綁定的多處理代碼所做的,但是這里你不需要擔(dān)心會(huì)話對(duì)象。
如上所述,處理 multiprocessing.pool()構(gòu)造函數(shù)的可選參數(shù)值得注意??梢灾付ㄒ诔刂袆?chuàng)建和管理的進(jìn)程對(duì)象的數(shù)量。默認(rèn)情況下,它將確定機(jī)器中有多少 CPU,并為每個(gè) CPU 創(chuàng)建一個(gè)進(jìn)程。雖然這對(duì)于我們的簡(jiǎn)單示例來(lái)說(shuō)很有用,但你可能希望在生產(chǎn)環(huán)境它也能發(fā)揮作用。
另外,和我們?cè)诘谝还?jié)中提到的線程一樣,multiprocessing.Pool 的代碼是建立在 Queue 和 Semaphore 上的,這對(duì)于使用其他語(yǔ)言執(zhí)行多線程和多處理代碼的人來(lái)說(shuō)是很熟悉的。
為什么多處理版本很重要
這個(gè)例子的多處理版本非常好,因?yàn)樗鄬?duì)容易設(shè)置,并且只需要很少的額外代碼。它還充分利用了計(jì)算機(jī)中的 CPU 資源。在我的機(jī)器上,運(yùn)行它只需要 2.5 秒:
這比我們看到的其他方法要好得多。
多處理版本的問(wèn)題
使用多處理有一些缺點(diǎn)。在這個(gè)簡(jiǎn)單的例子中,這些缺點(diǎn)并沒(méi)有顯露出來(lái),但是將你的問(wèn)題分解開(kāi)來(lái),以便每個(gè)處理器都能獨(dú)立工作有時(shí)是很困難的。此外,許多解決方案需要在流程之間進(jìn)行更多的通信,這相比非并發(fā)程序來(lái)說(shuō)會(huì)復(fù)雜得多。雷鋒網(wǎng)
何時(shí)使用并發(fā)性
首先,你應(yīng)該判斷是否應(yīng)該使用并發(fā)模塊。雖然這里的示例使每個(gè)庫(kù)看起來(lái)非常簡(jiǎn)單,但并發(fā)性總是伴隨著額外的復(fù)雜性,并且常常會(huì)導(dǎo)致難以找到的錯(cuò)誤。
堅(jiān)持添加并發(fā)性,直到出現(xiàn)已知的性能問(wèn)題,然后確定需要哪種類型的并發(fā)性。正如 DonaldKnuth 所說(shuō),「過(guò)早的優(yōu)化是編程中所有災(zāi)難(或者至少大部分災(zāi)難)的根源(Premature optimization is the root of all evil (or at least most of it) in programming)」。
一旦你決定優(yōu)化你的程序,弄清楚你的程序是 CPU 綁定的還是 I/O 綁定的,這就是下一步要做的事情。記住,I/O 綁定的程序是那些花費(fèi)大部分時(shí)間等待事情完成的程序,而 CPU 綁定的程序則盡可能快地處理數(shù)據(jù)。
正如你所看到的,CPU 綁定的問(wèn)題實(shí)際上只有在使用多處理才能解決。線程和異步根本沒(méi)有幫助解決這類問(wèn)題。
對(duì)于 I/O 綁定的問(wèn)題,python 社區(qū)中有一個(gè)通用的經(jīng)驗(yàn)規(guī)則:「可以使用異步,必須使用線程?!巩惒娇梢詾檫@種類型的程序提供最佳的速度,但有時(shí)需要某些關(guān)鍵庫(kù)來(lái)利用它。記住,任何不放棄對(duì)事件循環(huán)控制的任務(wù)都將阻塞所有其他任務(wù)。
CPU 綁定加速的內(nèi)容就到此為止啦,了解更多請(qǐng)?jiān)L問(wèn)原文!
前面的部分請(qǐng)查看:
如何利用并發(fā)性加速你的python程序(一):相關(guān)概念
如何利用并發(fā)性加速你的python程序(二):I/O 綁定程序加速
雷峰網(wǎng)版權(quán)文章,未經(jīng)授權(quán)禁止轉(zhuǎn)載。詳情見(jiàn)轉(zhuǎn)載須知。