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