如何利用併發性加速你的python程序(三):CPU 綁定程序加速

雷鋒網 AI 科技評論按,本文是工程師 Jim Anderson 分享的關於「通過併發性加快 python 程序的速度」的文章的第三部分,主要內容是 CPU 綁定程序加速相關。

在前面兩篇中,我們已經講過了相關的概念以及 I/O 綁定程序的加速,這篇是這一系列文章的最後一篇,講的是 CPU 程序加速。雷鋒網 AI 科技評論編譯整理如下:

如何加速 CPU 綁定程序

到目前為止,前面的例子都處理了一個 I/O 綁定問題。現在,你將研究 CPU 綁定的問題。如你所見,I/O 綁定的問題大部分時間都在等待外部操作(如網絡調用)完成。另一方面,CPU 限制的問題只執行很少的 I/O 操作,它的總體執行時間取決於它處理所需數據的速度。

在我們的示例中,我們將使用一個有點愚蠢的函數來創建一些需要在 CPU 上運行很長時間的東西。此函數計算從 0 到傳入值的每個數字的平方和:

你將處理一大批數據,所以這需要一段時間。記住,這只是代碼的一個佔位符,它實際上做了一些有用的事情,需要大量的處理時間,例如計算公式的根或對大型數據結構進行排序。

CPU 綁定的同步版本

現在讓我們看一下這個示例的非併發版本:

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 上單個進程中的單個線程上完成所有這些工作。執行時序圖如下:

與 I/O 綁定示例不同,CPU 綁定示例的運行時間通常相當一致。這台機器大約需要 7.8 秒:

顯然我們可以做得更好。這都是在沒有併發性的單個 CPU 上運行的。讓我們看看我們能做些什麼來改善它。

線程和異步版本

你認為使用線程或異步重寫此代碼會加快速度嗎?

如果你回答「一點也不」,這是有道理的。如果你回答,「它會減慢速度,」那就更對啦。

原因如下:在上面的 I/O 綁定示例中,大部分時間都花在等待緩慢的操作完成上。線程和異步通過允許你重疊等待的時間而不是按順序執行,這能加快速度。

但是,在 CPU 綁定的問題上,不需要等待。CPU 會儘可能快速地啟動以解決問題。在 python 中,線程和任務都在同一進程中的同一個 CPU 上運行。這意味着一個 CPU 不僅做了非併發代碼的所有工作,還需要做線程或任務的額外工作。它花費的時間超過 10 秒:

我已經編寫了這個代碼的線程版本,並將它與其他示例代碼放在 Github repo 中,這樣你就可以自己測試它了。

CPU 綁定的多處理版本

現在,你終於要接觸多處理真正與眾不同的地方啦。與其他併發庫不同,多處理被顯式設計為跨多個 CPU 共同承擔工作負載。它的執行時序圖如下所示:

它的代碼是這樣的:

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")

這些代碼和非併發版本相比幾乎沒有要更改的。你必須導入多處理,然後把數字循環改為創建多處理.pool 對象,並使用其.map()方法在工作進程空閑時將單個數字發送給它們。

這正是你為 I/O 綁定的多處理代碼所做的,但是這裡你不需要擔心會話對象。

如上所述,處理 multiprocessing.pool()構造函數的可選參數值得注意。可以指定要在池中創建和管理的進程對象的數量。默認情況下,它將確定機器中有多少 CPU,並為每個 CPU 創建一個進程。雖然這對於我們的簡單示例來說很有用,但你可能希望在生產環境它也能發揮作用。

另外,和我們在第一節中提到的線程一樣,multiprocessing.Pool 的代碼是建立在 Queue 和 Semaphore 上的,這對於使用其他語言執行多線程和多處理代碼的人來說是很熟悉的。

為什麼多處理版本很重要

這個例子的多處理版本非常好,因為它相對容易設置,並且只需要很少的額外代碼。它還充分利用了計算機中的 CPU 資源。在我的機器上,運行它只需要 2.5 秒:

這比我們看到的其他方法要好得多。

多處理版本的問題

使用多處理有一些缺點。在這個簡單的例子中,這些缺點並沒有顯露出來,但是將你的問題分解開來,以便每個處理器都能獨立工作有時是很困難的。此外,許多解決方案需要在流程之間進行更多的通信,這相比非併發程序來說會複雜得多。雷鋒網

何時使用併發性

首先,你應該判斷是否應該使用併發模塊。雖然這裡的示例使每個庫看起來非常簡單,但併發性總是伴隨着額外的複雜性,並且常常會導致難以找到的錯誤。

堅持添加併發性,直到出現已知的性能問題,然後確定需要哪種類型的併發性。正如 DonaldKnuth 所說,「過早的優化是編程中所有災難(或者至少大部分災難)的根源(Premature optimization is the root of all evil (or at least most of it) in programming)」。

一旦你決定優化你的程序,弄清楚你的程序是 CPU 綁定的還是 I/O 綁定的,這就是下一步要做的事情。記住,I/O 綁定的程序是那些花費大部分時間等待事情完成的程序,而 CPU 綁定的程序則儘可能快地處理數據。

正如你所看到的,CPU 綁定的問題實際上只有在使用多處理才能解決。線程和異步根本沒有幫助解決這類問題。

對於 I/O 綁定的問題,python 社區中有一個通用的經驗規則:「可以使用異步,必須使用線程。」異步可以為這種類型的程序提供最佳的速度,但有時需要某些關鍵庫來利用它。記住,任何不放棄對事件循環控制的任務都將阻塞所有其他任務。

CPU 綁定加速的內容就到此為止啦,了解更多請訪問原文

前面的部分請查看:

如何利用併發性加速你的python程序(一):相關概念

如何利用併發性加速你的python程序(二):I/O 綁定程序加速


想在手機閱讀更多程式設計資訊?下載【香港矽谷】Android應用
分享到Facebook
技術平台: Nasthon Systems