- Python高性能編程(第2版)
- (美)米夏·戈雷利克等
- 2602字
- 2023-09-06 19:21:25
1.2 綜合考慮
要完全明白高性能編程面臨的問題,僅熟悉計算機的基本部件還不夠,還需知道它們如何相互影響以及如何協同工作來解決問題。本節將探索一些簡單問題,看看它們的理想解決方案是如何工作的,以及Python又是如何解決它們的。
本節可能讓你感到絕望,因為它好像主要是說Python無法應對性能方面的問題。但實際情況并非如此,其原因有兩個。首先,這里討論高性能計算時,忽略了一個重要的因素——開發人員。Python本身的性能雖然不高,但使用它可快速開發程序,這就彌補了這種缺陷。其次,通過利用本書后面將介紹的模塊和理念,可輕松地消除這里介紹的眾多問題。因此,使用Python可快速開發程序,同時規避眾多性能約束。
理想的計算模型與Python虛擬機
為了更深入地理解高性能編程的因素,來看一段簡單的代碼,它判斷一個數是否是素數:
import math def check_prime(number): sqrt_number = math.sqrt(number) for i in range(2, int(sqrt_number) + 1): if (number / i).is_integer(): return False return True print(f"check_prime(10,000,000) = {check_prime(10_000_000)}") # check_prime(10,000,000) = False print(f"check_prime(10,000,019) = {check_prime(10_000_019)}") # check_prime(10,000,019) = True
我們先根據抽象計算模型分析這段代碼,再將其與Python運行這段代碼的情況進行比較。與其他抽象一樣,這里也將忽略理想計算模型和Python運行這些代碼時涉及的眾多細節。在解決問題前,先像這里這樣做通常都是一個不錯的主意:想想算法的基本組成部分以及最佳的解決方案是什么樣的。知道理想情況以及Python中的實際情況后,就可不斷調整Python代碼,使其更接近最優狀態。
1.理想的計算模型
在上述代碼中,首先將number的值存儲到了RAM中。為計算sqrt_number,需要將number的值傳遞給CPU。在理想情況下,只需傳遞這個值一次:它將存儲在CPU的L1/L2緩存中,而CPU將執行計算并將結果傳回給RAM進行存儲。這是最理想的情況,最大限度地減少了從RAM中讀取number值的次數,轉而從L1/L2緩存中讀取這個值,而這樣做的速度要快得多。另外,通過使用與CPU直接相連的L1/L2緩存,最大限度地減少了通過前端總線傳輸數據的次數。
提示:對優化來說,確保數據在合適的地方并盡可能少地移動它們至關重要。所謂“繁重的數據”指的是花費大量時間和精力來移動數據,這是需要避免的。
對于上述代碼的循環部分,我們希望一次性將number和多個i值傳遞給CPU,而不是每次傳遞一個i值。這是因為CPU能夠向量化操作,且不會增加時間開銷,換句話說,CPU能夠同時執行多項獨立的計算。因此,我們希望將number傳遞給CPU緩存,并根據緩存的容量傳遞盡可能多的i值。對于所有number和i值組合,都將它們相除并檢查結果是否是整數,再返回一個信號,指出是否有結果是整數。如果有結果是整數,整個函數就到此結束;如果沒有,就重復前述過程。通過這樣做,可針對多個i值返回一個結果,避免了通過速度緩慢的總線返回每個i值的結果。這里利用了CPU的向量化功能,即在一個時鐘周期內對多項數據執行同一條指令。
下面的代碼演示了向量化的概念:
import math def check_prime(number): sqrt_number = math.sqrt(number) numbers = range(2, int(sqrt_number)+1) for i in range(0, len(numbers), 5): # the following line is not valid Python code result = (number / numbers[i:(i + 5)]).is_integer() if any(result): return False return True
這里對流程做了設置,使得每次循環根據5個i值執行除法運算并檢查是否有結果為整數。如果正確地進行了向量化,CPU就能一步執行完整行代碼,而無須針對每個i值分別進行計算。理想情況下,CPU可獨立完成操作any(result),而無須將結果傳回給RAM。第6章將更詳細地介紹向量化,包括其工作原理以及在什么情況下使用它可提高代碼的性能。
2.Python虛擬機
Python解釋器做了大量的工作,力圖對程序員隱藏它使用的計算部件。這讓程序員根本不用考慮如下問題:如何給數組分配內存、如何組織這些內存以及其中的數據是以什么樣的順序發送給CPU的。這是Python的一個優勢,讓程序員能夠專注于要實現的算法,但付出的代價是性能可能急劇下降。
需要指出的是,Python運行的確實是經過極度優化的指令,但你需要掌握一些訣竅,讓Python以正確的順序執行這些指令,以進一步提高性能。例如,在下面的示例中,search_fast的速度比search_slow快,這很容易判斷出來,這是因為雖然這兩個函數的運行時間都是O(n),但search_fast通過提前結束循環避免了多余的計算。然而,在涉及派生類型、特殊的Python方法或第三方模塊時,情況可能更復雜。例如,對于下面的函數search_unknown1和search_unknown2,你能迅速判斷出哪個的速度更快嗎?
def search_fast(haystack, needle): for item in haystack: if item == needle: return True return False def search_slow(haystack, needle): return_value = False for item in haystack: if item == needle: return_value = True return return_value def search_unknown1(haystack, needle): return any((item == needle for item in haystack)) def search_unknown2(haystack, needle): return any([item == needle for item in haystack])
前面演示的是找出無用操作并將其刪除,與之類似的是通過剖析找出速度緩慢的代碼,并尋找效率更高的計算方式。雖然最終的結果是一樣的,但通過這樣做,可極大地減少計算次數和數據傳輸次數。
前述抽象層帶來的影響之一是,無法直接利用向量化。在前述判斷素數的函數中,對于每個i值都將運行一次循環迭代,而不會將多個迭代合并。如果你再看看前述向量化示例,將發現它并非合法的Python代碼,因為在Python中,不能將浮點數與列表相除。在這種情況下,諸如numpy等外部庫可提供幫助,它讓你能夠執行向量化數學運算。
另外,Python所做的抽象還會影響這樣的優化,即它依賴于將相關的數據保留在L1/L2緩存中,以供下一次計算時使用。導致這種結果的原因很多。首先,Python對象在內存中并不是以最優方式排列的。這是因為Python是一種垃圾收集語言——根據需要自動分配和釋放內存。這會導致內存碎片,進而可能影響將數據傳輸到CPU緩存的過程。與此同時,你無法直接調整數據結構在內存中的排列,這意味著即便與特定計算相關的數據量低于總線寬度,也可能無法通過總線一次性傳輸它們[4]。
[4] 第6章將演示如何重獲這種控制權,進而對代碼進行優化,細致到其內存使用模式。
其次,Python不是編譯型語言,且其使用的類型是動態的。憑借多年的經驗,很多C語言程序員都發現,編譯器通常比自己聰明。編譯靜態代碼時,編譯器能夠巧妙地調整布局以及CPU運行指令的方式,從而對代碼進行優化。Python不是編譯型語言,雪上加霜的是,其類型是動態的,這意味著根據算法推斷出可能的優化機會要難得多,因為在運行期間,代碼的功能可能發生變化。緩解這種問題的途徑有很多,其中居首的是使用Cython,它讓Python代碼能夠被編譯,還讓開發人員能夠將代碼的動態程度告知編譯器。
最后,前面提到的GIL也可能影響并行代碼的性能。例如,假設為利用多個CPU核心對前述代碼進行修改,讓每個核心處理2~sqrtN的一個子范圍。每個核心都可獨立地處理分配給它的子范圍,再在處理完畢后比較結果。雖然這樣做會失去提前結束循環的好處(因為每個核心都不知道其他核心的處理結果),但可減少縮小每個核心需要處理的數字范圍(如果有M個核心,則每個核心需要做的檢查將為sqrtN/M次)。但是,由于GIL的存在,不能同時使用多個核心。這意味著效果與非并行版本相同,而且不能提前結束循環。為避免這種問題,可使用模塊multiprocessing實現多進程(而不是多線程),還可使用Cython或外部函數。
- Google Apps Script for Beginners
- Microsoft Exchange Server PowerShell Cookbook(Third Edition)
- Rust實戰
- Cross-platform Desktop Application Development:Electron,Node,NW.js,and React
- 區塊鏈架構與實現:Cosmos詳解
- Visual Basic程序設計(第3版):學習指導與練習
- Learning AWS Lumberyard Game Development
- Java程序設計與計算思維
- 量化金融R語言高級教程
- Visual Basic程序設計上機實驗教程
- Mastering Unity 2D Game Development(Second Edition)
- Java Web開發就該這樣學
- Python項目實戰從入門到精通
- R數據科學實戰:工具詳解與案例分析
- 機器學習微積分一本通(Python版)