比較各種Python求冪方法的性能

Python 算法 勒內·笛卡兒 今日頭條 Python部落 2019-06-16

最近,我在寫一個算法來解決一個編碼難題,這個難題涉及到在一個笛卡爾平面上找到一個與其他所有點的距離最小的點。在Python中,兩個點之間的距離函數可以表示為math.sqrt(dx ** 2 + dy ** 2)。但是,這個函數中的每一項都有不同的表達方法:dx ** 2、 math.pow(dx, 2)和 dx * dx。有趣的是,它們的運行結果各不相同,我想知道它們是如何以及為什麼會是這樣的。

計時測試

Python提供了一個名為 timeit 的模塊來測試性能,這使得測試這些表達式的運行時間相當簡單。將 x 設置為 2,我們就可以對上面的三個選項進行計時測試:

比較各種Python求冪方法的性能

表達式反彙編

Python還提供了一個名為 dis 的模型,它可以對代碼進行反彙編,這樣我們就可以看到每一個表達式在底層做些什麼,這有助於我們理解其性能差異。

乘法

使用 dis.dis(lambda x: x * x),我們可以看到以下代碼被執行:

比較各種Python求冪方法的性能

該程序將 x 載入兩次,執行 BINARY_MULTIPLY 操作,並且返回得到的值。

math.pow()

使用 dis.dis(lambda x: math.pow(x, 2)), 我們可以看到以下代碼被執行:

比較各種Python求冪方法的性能

math 模塊從全局空間開始加載變量,然後加載pow屬性。接下來,加載兩個參數並調用pow函數,該函數會返回計算值。

(此處已添加圈子卡片,請到今日頭條客戶端查看)

求冪

使用dis.dis(lambda x: x ** 2), 我們可以看到以下代碼被執行:

比較各種Python求冪方法的性能

該程序先加載 x, 再加載2,然後運行 BINARY_POWER 並返回計算結果。.

BINARY_MULTIPLY 與 BINARY_POWER

使用math.pow()函數作為一個比較點,乘法和求冪的字節碼的只有一部分有所不同: 調用BINARY_MULTIPLY 與調用BINARY_POWER。

BINARY_MULTIPLY

這個函數位於這裡(https://github.com/Python/cPython/blob/b509d52083e156f97d6bd36f2f894a052e960f03/Objects/longobject.c#L3645-L3665 )的Python源代碼中。它做了一些有趣的事情:

比較各種Python求冪方法的性能

對於較小的數,這個函數使用二進制乘法。對於較大的值,該函數使用Karatsuba乘法,這是一種針對較大數字的快速乘法算法。

我們可以看到這個函數是如何在 ceval.c中被調用的:

比較各種Python求冪方法的性能

BINARY_POWER

這個函數位於這裡(https://github.com/Python/cPython/blob/b509d52083e156f97d6bd36f2f894a052e960f03/Objects/longobject.c#L4118-L4305 )的Python源代碼中。它還做了一些有趣的事情:

源代碼太長,所以無法完全包括進來,這在一定程度上解釋了這種不利的性能。以下是一些有趣的代碼片段:

比較各種Python求冪方法的性能

在創建了一些指針之後,該函數會檢查power給出的數是浮點數還是負數,在這裡它可能會出錯,也可能會調用另一個函數來處理求冪操作。

如果兩種情況都不是,該函數將檢查第三個參數,根據ceval.c的代碼來看這個參數通常是None:

比較各種Python求冪方法的性能

最後,該函數定義了兩個例程: REDUCE用於模降,MULT用於乘法和減法。乘法函數對兩個值使用了long_mul函數,這與BINARY_MULTIPLY中使用的函數相同。

比較各種Python求冪方法的性能

之後,該函數使用《應用密碼學手冊》第14.6章中定義的從左到右k次求冪方法:

比較各種Python求冪方法的性能

圖表化性能差異

我們可以使用上面的timeit庫分析在不同值時的代碼,並查看性能如何隨時間變化。

生成函數

為了測試不同power值下的性能,我們需要生成一些函數。

math.pow()和取冪

由於這兩個函數都已經在Python源代碼中,所以我們需要做的就是定義一個求冪函數,並且我們可以在一個timeit調用中來調用它。“

比較各種Python求冪方法的性能

連乘

由於每一次power發生變化時,這個函數也會發生變化。所以每一次函數中的 base 發生變化時,我們都需要生成一個新的乘法函數。為此,我們可以生成一個像 x*x*x 這樣的字符串,並對它調用eval()來返回一個函數:

比較各種Python求冪方法的性能

這樣,我們就可以像這樣來創建一個 multiply 函數:

比較各種Python求冪方法的性能

如果我們調用 generate_mult_func(4), multiply函數將會是一個類似於這樣的匿名函數:

比較各種Python求冪方法的性能

查找交叉點

使用這裡(https://repl.it/@reagentx/Find-Crossover )貼出的代碼,我們可以決定 multiply 在什麼情況下會變得比 exponent 效率更低。

我們先從這些值開始:

比較各種Python求冪方法的性能

我們持續循環,直到該函數執行100,000次 multiply 迭代的時間比執行100,000次 exponent 迭代的時間慢。首先,這裡是計時時間,使用math.pow()作為一個比較點:

比較各種Python求冪方法的性能

當我們在repl.it上運行時, Python在1.2s內找到了交叉點:

比較各種Python求冪方法的性能

因此,在我們的表達式達到2^14時,連乘是運行最快的,而在到達2^15時, 求冪變成最快的。

圖表化性能

使用Pandas, 我們可以跟蹤每一次求冪的時間:

比較各種Python求冪方法的性能

使用下面的代碼來生成一條折線圖是非常簡單的:

比較各種Python求冪方法的性能

比較各種Python求冪方法的性能

有趣的是, math.pow() 和 exponent 的大多數操作都是以相同的速率來執行的 ,而我們的multiply函數差異很大。不出所料,乘法鏈越長,執行所需的時間就越長。

更多的性能測試

雖然交叉很有趣,但這並沒有顯示冪大於15時的情況。冪上升到1000,我們得到以下趨勢:

比較各種Python求冪方法的性能

當我們放大以使math.pow()和exponent更明顯時,我們看到它們有相同的性能趨勢並仍在繼續:

比較各種Python求冪方法的性能

雖然使用 ** 時時間會逐漸增加,但是math.pow()通常是以相同的速度執行。

結論

當編寫使用小指數的算法時,這裡證明為冪小於15時,進行連乘比使用**指數運算符更快。此外,在冪大於5的情況下,math.pow()比連乘更有效,而且它總是比**操作符更有效,所以沒有任何理由去使用**。

此外,在JavaScript中也是如此。感謝@juliancoleman在這裡(https://jsperf.com/mult-vs-expo 0做的這個比較!



英文原文:https://chrissardegna.com/blog/posts/python-expontentiation-performance/ 譯者:野生大熊貓

相關推薦

推薦中...