官术网_书友最值得收藏!

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或外部函數。

主站蜘蛛池模板: 宝兴县| 微山县| 晋中市| 上虞市| 从江县| 锡林郭勒盟| 吴江市| 象山县| 开平市| 丹寨县| 清水县| 延安市| 甘肃省| 宣武区| 恩施市| 丰都县| 会泽县| 梧州市| 六枝特区| 延边| 靖远县| 河北省| 贺兰县| 桃江县| 二连浩特市| 新民市| 收藏| 册亨县| 泰州市| 东乡县| 抚顺市| 绥江县| 柳河县| 六安市| 囊谦县| 武宁县| 革吉县| 偃师市| 蓬莱市| 巴东县| 邢台县|