Manjusaka

Manjusaka

為什麼有些時候 Python 中乘法比位運算更快

我本來以為我不再會寫水文了,但是突然發現自己現在也只能勉強寫寫水文才能維持生活這樣子。那就繼續寫水文吧

某天,一個技術群裡老哥提出了這樣一個問題,為什麼在一些情況下,Python 中的簡單乘 / 除法比位運算要慢

首先秉持著實事求是的精神,我們先來驗證一下

我們發現幾個很有趣的現象

  1. 在值 x<=2^30 時,乘法比直接位運算要快
  2. 在值 x>2^32 時,乘法顯著慢於位運算

這個現象很有趣,那麼這個現象的 root cause 是什麼?實際上這和 Python 底層的實現有關

簡單聊聊#

PyLongObject 的實現#

在 Python 2.x 時期,Python 中將整型分為兩類,一類是 long, 一類是 int 。在 Python3 中這兩者進行了合併。目前在 Python3 中這兩者做了合併,僅剩一個 long

首先來看看 long 這樣一個數據結構底層的實現

在這裡不用關心,PyObject_VAR_HEAD 的含義,我們只需要關心 ob_digit 即可。

在這裡,ob_digit 是使用了 C99 中的 “柔性數組” 來實現任意長度的整數的存儲。這裡我們可以看一下官方代碼中的文檔

Long integer representation.The absolute value of a number is equal to SUM(for i=0 through abs(ob_size)-1) ob_digit[i] * 2**(SHIFT*i)
Negative numbers are represented with ob_size < 0; zero is represented by ob_size == 0.
In a normalized number, ob_digit[abs(ob_size)-1] (the most significant digit) is never zero. Also, in all cases, for all valid i,0 <= ob_digit[i] <= MASK.
The allocation function takes care of allocating extra memory so that ob_digit[0] ... ob_digit[abs(ob_size)-1] are actually available.
CAUTION: Generic code manipulating subtypes of PyVarObject has to aware that ints abuse ob_size's sign bit.

簡而言之,Python 是將一個十進制數轉為 2^(SHIFT) 進制數來進行存儲。這裡可能不太好理解。我來舉個例子,在我的電腦上,SHIFT 為 30 ,假設現在有整數 1152921506754330628 ,那麼將其轉為 2^30 進制表示則為: 4*(2^30)^0+2*(2^30)^1+1*(2^30)^2 。那麼此時 ob_digit 是一個含有三個元素的數組,其值為 [4,2,1]

OK,在明白了這樣一些基礎知識後,我們回過頭去看看 Python 中的乘法運算

Python 中的乘法運算#

Python 中的乘法運算,分為兩部分,其中關於大數的乘法,Python 使用了 Karatsuba 算法1,具體實現如下

這裡不對 Karatsuba 算法1 的實現做單獨解釋,有興趣的朋友可以參考文末的 reference 去了解具體的詳情。

在普通情況下,普通乘法的時間複雜度位 n^2 (n 為位數),而 K 算法的時間複雜度為 3n^(log3) ≈ 3n^1.585 ,看起來 K 算法的性能要優於普通乘法,那麼為什麼 Python 不全部使用 K 算法呢?

很簡單,K 算法的優勢實際上要在當 n 足夠大的時候,才會對普通乘法形成優勢。同時考慮到內存訪問等因素,當 n 不夠大時,實際上採用 K 算法的性能將差於直接進行乘法。

所以我們來看看 Python 中乘法的實現

在這裡我們看到,當兩個數皆小於 2^30-1 時,Python 將直接使用普通乘法並返回,否則將使用 K 算法進行計算

這個時候,我們來看一下位運算的實現,以右移為例

在這裡我們能看到,在兩側都是小數的情況下,位移運算算法將比普通乘法,存在更多的內存分配等操作。這樣也會回答了我們文初所提到的一個問題,“為什麼一些時候乘法比位運算更快”。

總結#

本文差不多就到這裡了,實際上通過這次分析我們能得到一些很有趣但是也很冷門的知識。實際上我們目前看到這樣一個結果,是 Python 對於我們常見且高頻的操作所做的一個特定的設計。而這也提醒我們,Python 實際上對於很多操作都存在自己內建的設計哲學,在日常使用的時候,其餘語言的經驗,可能無法復用

差不多就這樣吧,只能勉強寫水文苟活了(逃

Reference#

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。