?Pyrex 是一種專門設計用來編寫 Python 擴展模塊的語言。根據 Pyrex Web 站點的介紹,“它被設計用來在友好易用的高級 Python 世界和凌亂的低級 C 世界之間搭建一個橋梁?!彪m然幾乎所有的 Python 代碼都可以作為有效的 Pyrex 代碼使用,但是您可以在 Pyrex 代碼中添加可選的靜態類型聲明,從而使得這些聲明過的對象以 C 語言的速度運行。
加速 Python
從某種意義上來說,Pyrex 只是不斷發展的 Python 類語言系列的一個部分:Jython、IronPython、Prothon、Boo、Vyper(現在沒人用了)、Stackless Python(以一種方式)或 Parrot runtime(以另外一種方式)。按照語言的術語來說,Pyrex 本質上是在 Python 中添加了類型聲明。它的另外幾個變化沒有這么重要(不過對 for 循環的擴展很漂亮)。
然而,您真正希望使用 Pyrex 的原因是它編寫的模塊比純 Python 運行得更快,可能會快很多。
實際上,Pyrex 會從 Pyrex 代碼生成一個 C 程序。中間文件 module.c 依然可以用于手工處理。然而對于“普通的” Pyrex 用戶來說,沒有什么理由需要修改所生成的 C 模塊。Pyrex 本身可以讓您訪問那些對速度至關重要的 C 級代碼,而節省了編寫內存分配、回收、指針運算、函數原型等的工作。Pyrex 還可以無縫地處理 Python 級對象的所有接口;通常它都是通過在必要的地方將變量聲明為 PyObject 結構并使用 Python C-API 調用進行內存處理和類型轉換而實現的。
對于大部分情況來說,Pyrex 不需要不斷對簡單數據類型變量進行裝箱(box) 和 拆箱(unbox) 操作,因此速度比 Python 更快。例如,Python 中的 int 類型是一個具有很多方法的對象。它有一個繼承樹,自己有一個計算好的“方法解析順序(mothod resolution order,MRO)”。它有分配和回收方法可以用于內存處理。它知道何時將自己轉換為一個 long 類型,以及如何對其他類型的值進行數值運算。所有這些額外的功能都意味著在使用 int 對象進行處理時需要經過更多級的間接處理或條件檢查。另外一方面,C 或 Pyrex 的 int 變量只是內存中各個位設置為 1 或 0 的一個區域。使用 C/Pyrex 的 int 類型進行處理不需要涉及 任何 間接操作或條件檢查。一個 CPU “加”操作在硅芯片中就可以執行完了。
在仔細選擇的情況中,Pyrex 模塊的速度可以比 Python 版本的相同模塊的運行速度快 40 到 50 倍。但是與使用 C 本身 編寫的模塊相比,Pyrex 版本的模塊幾乎都不會比 Python 版本的模塊更長,代碼更類似于 Python,而不是 C。
當然,當您開始談論加速(類)Python 模塊時,Pyrex 并不是惟一可用的工具。在 Python 開發者的選擇中,也可以使用 Psyco。Psyco 可以保持代碼非常簡短;它是(x86)機器代碼中的一個 JIT Python 代碼編譯器。與 Pyrex 不同,Psyco 并不會精確地限定變量的類型,而是根據數據 可能 是哪種類型的每種假設為每個 Python 代碼塊創建幾種可能的機器代碼。如果在一個給定的代碼段中數據是是簡單類型,例如 int,那么這段代碼(如果是一個循環,這種情況就更為突出)就可以很快地運行。例如,x 在一個執行一百萬次的循環中可以是 int 類型,但是在循環結束時可以依然是一個 float 類型的值。Psyco 可以使用與在 Pyrex 中顯式指定的類型相同的類型來加速循環。
雖然 Pyrex 也并不難,但是 Psyco 更加簡單易用。使用 Psyco 不過是在模塊的末尾加上幾行;實際上,如果加上正確的代碼,那么即使在 Psyco 不可用時,模塊也可以同樣運行(只是速度較慢)。
清單 1. 只有在 Psyco 可用時才使用 Psyco
# Import Psyco if available try: import psyco psyco.full() except ImportError: pass
要使用 Pyrex,需要對代碼進行的修改會更多(但也不過是多一點而已),系統中還需要安裝一個 C 編譯器,并正確對生成 Pyrex 模塊的系統進行配置。雖然您 可以 分發二進制的 Pyrex 模塊,但是為了能使您的模塊在其他地方也可以運行,Python 的版本、架構和終端用戶需要的優化選項必須匹配。
速度初體驗
我最近為 developerWorks 的文章 Beat spam using hashcash 創建了一個純 Python 的 hashcash 實現,但是基本上來說,hashcash 是一種使用 SHA-1 提供 CPU 工作的技術。Python 有一個標準的模塊 sha,這使得編寫 hashcash 非常簡單。
與我編寫的 95% 的 Python 程序不同,hashcash 模塊緩慢的速度讓我心煩,至少有那么一點點心煩。按照設計,這個協議就是要吃光所有的 CPU 周期,因此運行效率非常關鍵。hashcash.c 的 ANSI C 二進制文件運行的速度是這個 hashcash.py 腳本的 10 倍。而且啟用了 PPC/Altivec 的優化后的 hashcash.c 二進制文件的速度是普通的 ANSI C 版本的 4 倍(1Ghz 的 G4/Altivec 在處理 hashcash/SHA 操作時的速度相當于 3Ghz 的 Pentium4?/MMX;G5 的速度會更快)。因此在我的 TiPowerbook 上的測試顯示,這個模塊的速度比優化后的 C 版本速度慢 40 倍(不過在 x86 上的差距沒有這么大)。
由于這個模塊的運行速度很慢,可能 Pyrex 會是一個比較好的加速方法。至少我認為是如此?!癙yrex 化” hashcash.py 的第一件事情(當然是在安裝 Pyrex 之后)是簡單地將其拷貝為 hashcash_pyx.pyx,并試圖這樣處理:
$ pyrexc hashcash_pyx.pyx
創建二進制模塊
運行這個命令會生成一個 hashcash.c 文件(這會對源文件進行一些微小的改動)。不幸的是,調整 gcc 開關剛好適合我的平臺需要點技巧,因此我決定采用推薦的捷徑,讓 distutils 為我做一些工作。標準的 Python 安裝知道如何在模塊安裝過程中使用本地的 C 編譯器,以及如何使用 distutils 來簡化 Pyrex 模塊的共享。我創建了一個 setup_hashcash.py 腳本,如下所示:
清單 2. setup_hashcash.py 腳本
from distutils.core import setup from distutils.extension import Extension from Pyrex.Distutils import build_ext setup( name = "hashcash_pyx", ext_modules=[ Extension("hashcash_pyx", ["hashcash_pyx.pyx"], libraries = []) ], cmdclass = {'build_ext': build_ext} )
運行下面的命令,完整地編譯一個基于 C 的擴展模塊 hashcash:
$ python2.3 prime_setup.py build_ext --inplace
代碼修改
我把從 hashcash.pyx 生成基于 C 的模塊的工作有些簡化了。實際上,我需要對源代碼進行兩處修改;通過查找 pyrexc 抱怨的位置來找到要修改的位置。在代碼中,我使用了一個不支持的列表,將其放入一個普通的 for 循環。這非常簡單。我還將增量賦值從 counter+=1 修改為 counter=counter+1。
就這么多了。這就是我的第一個 Pyrex 模塊。
測試速度
為了可以簡單地測試要開發的模塊的速度提高情況,我編寫了一個簡單的測試程序來運行不同版本的模塊:
清單 3. 測試程序 hashcash_test.py
#!/usr/bin/env python2.3 import time, sys, optparse hashcash = __import__(sys.argv[1]) start = time.time() print hashcash.mint('mertz@gnosis.cx', bits=20) timer = time.time()-start sys.stderr.write("%0.4f seconds (%d hashes per second)\n" % (timer, hashcash.tries[0]/timer))
令人興奮的是,我決定來看一下只通過 Pyrex 編譯可以怎樣提高速度。注意在下面所有的例子中,真實的時間變化很大,都是隨機的。我們要看的內容是“hashes per second”,它可以精確可靠地測量速度。因此比較一下純粹的 Python 和 Pyrex:
清單 4. 純 Python 和 “純 Pyrex”的比較
$ ./hashcash_test.py hashcash 1:20:041003:mertz@gnosis.cx::I+lyNUpV:167dca 13.7879 seconds (106904 hashes per second) $ ./hashcash_test.py hashcash_pyx > /dev/null 6.0695 seconds (89239 hashes per second)
噢!使用 Pyrex 幾乎慢了 20%。這并不是我期望的。現在應該來分析一下代碼可能加速的地方了。下面這個簡短的函數會試圖消耗所有的時間:
清單 5. hashcash.py 中的函數
def _mint(challenge, bits): "Answer a 'generalized hashcash' challenge'" counter = 0 hex_digits = int(ceil(bits/4.)) zeros = '0'*hex_digits hash = sha while 1: digest = hash(challenge+hex(counter)[2:]).hexdigest() if digest[:hex_digits] == zeros: tries[0] = counter return hex(counter)[2:] counter += 1
我需要利用 Pyrex 變量聲明的優點來進行加速。有些變量顯然是整數,另外一些變量顯然是字符串 ―― 我們可以指定這些類型。在進行修改時,我將使用 Pyrex 的經過改進的 for 循環:
清單 6. 經過最低限度 Pyrex 改進的 mint 函數
cdef _mint(challenge, int bits): # Answer a 'generalized hashcash' challenge'" cdef int counter, hex_digits, i cdef char *digest hex_digits = int(ceil(bits/4.)) hash = sha for counter from 0 <= counter < sys.maxint: py_digest = hash(challenge+hex(counter)[2:]).hexdigest() digest = py_digest for i from 0 <= i < hex_digits: if digest[i] != c'0': break else: tries[0] = counter return hex(counter)[2:]
到現在為止一切都非常簡單。我只聲明了早已知道的一些變量類型,并使用最干凈的 Pyrex counter 循環。一個小技巧是將 py_digest(一個 Python 字符串)賦值給 digest(一個 C/Pyrex 字符串),目的是確定其類型。經過實驗,我還發現循環字符串比較操作速度都非??臁_@些會帶來什么好處呢?
清單 7. Pyrex 化 mint 函數的速度結果
$ ./hashcash_test.py hashcash_pyx2 >/dev/null 20.3749 seconds (116636 hashes per second)
這下好多了。我已經對原有的 Python 進行了一些細微的改進,這可以稍微提高最初的 Pyrex 模塊的速度。不過效果還不明顯,僅僅提高了很少的百分比。
剖析
有些東西似乎不對。速度提高幾個百分比和 Pyrex 主頁(以及很多 Pyrex 用戶)那樣提高 40 倍有很大的差距?,F在應該來看一下 這個 Python _mint() 函數中 哪些 地方真正消耗了時間。有一個 quick 腳本(此處沒有給出)可以分解復雜操作 sha(challenge+hex(counter)[2:]).hexdigest():
清單 8. hashcash 的 mint 函數的時間消耗
1000000 empty loops: 0.559 ------------------------------ 1000000 sha()s: 2.332 1000000 hex()[2:]s: 3.151 just hex()s: <2.471> 1000000 concatenations: 0.855 1000000 hexdigest()s: 3.742 ------------------------------ Total: 10.079
顯然,我并不能將這個循環從 _mint() 函數中刪除。雖然 Pyrex 改進后的 for 循環可能有一點加速,但是整個函數主要是一個循環。我也不能刪除對 sha() 的調用,除非要使用 Pyrex 重新實現 SHA-1(即使我要這樣做,也沒有自信自己可以比 Python 標準的 sha 模塊的作者做得更好)。而且,如果我希望得到一個 sha.SHA 對象的 hash 值,就只能調用 .hexdigest() 或 .digest();前者的速度更快。
現在真正要解決的是 hex() 對 counter 變量的轉換,以及結果中時間片的消耗情況。我可能需要使用 Pyrex/C 的字符串連接操作,而不是 Python 的字符串對象。然而,我見過的惟一一種避免 hex() 轉換的方法是手工在嵌套循環之外構建一個后綴。雖然這樣做可以避免 int 到 char 類型的轉換,但是需要生成更多代碼:
清單 9. 完全 Pyrex 優化過的 mint 函數
cdef _mint(char *challenge, int bits): cdef int hex_digits, i0, i1, i2, i3, i4, i5 cdef char *ab, *digest, *trial, *suffix suffix = '******' ab = alphabet hex_digits = int(ceil(bits/4.)) hash = sha for i0 from 0 <= i0 < 55: suffix[0] = ab[i0] for i1 from 0 <= i1 < 55: suffix[1] = ab[i1] for i2 from 0 <= i2 < 55: suffix[2] = ab[i2] for i3 from 0 <= i3 < 55: suffix[3] = ab[i3] for i4 from 0 <= i4 < 55: suffix[4] = ab[i4] for i5 from 0 <= i5 < 55: suffix[5] = ab[i5] py_digest = hash(challenge+suffix).hexdigest() digest = py_digest for i from 0 <= i < hex_digits: if digest[i] != c'0': break else: return suffix
雖然這個 Pyrex 函數看起來仍然比對應的 C 函數更加簡單易讀,但是它實際上最初的純 Python 的版本更為復雜。通過這種方式,在純 Python 中展開后綴生成與最初的版本相比會對總體速度有些負面的影響。在 Pyrex 中,正如您期望的一樣,這些嵌套的循環都是很少花費時間的,因而我節省了轉換和分時調度的代價:
清單 10. mint 函數 Pyrex 化優化后的速度結果
$ ./hashcash_test.py hashcash_pyx3 >/dev/null 13.2270 seconds (166125 hashes per second)
當然,這比我開始的時候好多了。但是速度提高也不過是兩倍。大部分時間的問題是(此處也是)消耗了太多的時間在對 Python 庫的調用上,而我并不能對這些調用編寫代碼來提高速度。
令人失望的比較
速度提高 50% 到 60% 似乎是值得的。達到這個目標我并沒有編寫 多少 代碼。但是如果您認為是在原來的 Python 版本中添加 兩條 語句 import psyco;psyco.bind(_mint),那么這種加速方法就不會給您多深的印象:
清單 11. mint 函數 Psyco 化的加速結果
$ ./hashcash_test.py hashcash_psyco >/dev/null 15.2300 seconds (157550 hashes per second)
換而言之,Psyco 之不過添加了兩行通用的代碼,就幾乎能實現相同的目標。當然,Psyco 只能用于 x86 平臺,而 Pyrex 可以在具有 C 編譯器的所有環境上執行。但是對于這個特定的例子來說,os.popen('hashcash -m '+options) 的速度會比 Pyrex 和 Psyco 都快很多倍(當然,假設可以使用 C 工具 hashcash)。
?
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
