Psyco 是嚴(yán)格地在 Python 運(yùn)行時(shí)進(jìn)行操作的。也就是說,Python 源代碼是通過 python 命令編譯成字節(jié)碼的,所用的方式和以前完全相同(除了為調(diào)用 Psyco 而添加的幾個(gè) import 語句和函數(shù)調(diào)用)。但是當(dāng) Python 解釋器運(yùn)行應(yīng)用程序時(shí),Psyco 會不時(shí)地檢查,看是否能用一些專門的機(jī)器代碼去替換常規(guī)的 Python 字節(jié)碼操作。這種專門的編譯和 Java 即時(shí)編譯器所進(jìn)行的操作非常類似(一般地說,至少是這樣),并且是特定于體系結(jié)構(gòu)的。到現(xiàn)在為止,Psyco 只可用于 i386 CPU 體系結(jié)構(gòu)。Psyco 的妙處在于可以使用您一直在編寫的 Python 代碼(完全一樣?。瑓s可以讓它運(yùn)行得更快。
Psyco 是如何工作的
要完全理解 Psyco,您可能需要很好地掌握 Python 解釋器的 eval_frame() 函數(shù)和 i386 匯編語言。遺憾的是,我自己不能對其中任何一項(xiàng)發(fā)表專家性的意見 - 但是我想我可以大致不差地概述 Psyco。
在常規(guī)的 Python 中,eval_frame() 函數(shù)是 Python 解釋器的內(nèi)循環(huán)。eval_frame() 函數(shù)主要察看執(zhí)行上下文中的當(dāng)前字節(jié)碼,并將控制向外切換到一個(gè)適合實(shí)現(xiàn)該字節(jié)碼的函數(shù)。支持函數(shù)將做什么的具體細(xì)節(jié)通常取決于保存在內(nèi)存中的各種 Python 對象的狀態(tài)。簡單點(diǎn)說,添加 Python 對象“2”和“3”和添加對象“5”和“6”會產(chǎn)生不同的結(jié)果,但是這兩個(gè)操作都以類似的方式分派。
Psyco 用復(fù)合求值單元替代 eval_frame() 函數(shù)。Psyco 有幾種方法可以用來改進(jìn) Python 所進(jìn)行的操作。首先,Psyco 將操作編譯成有點(diǎn)優(yōu)化的機(jī)器碼;由于機(jī)器碼需要完成的工作和 Python 的分派函數(shù)所要做的事一樣,所以其本身只有些許改進(jìn)。而且,Psyco 編譯中的“專門的”內(nèi)容不僅僅是對 Python 字節(jié)碼的選擇,Psyco 也要對執(zhí)行上下文中已知的變量值進(jìn)行專門化。例如,在類似于下面的代碼中,變量 x 在循環(huán)持續(xù)時(shí)間內(nèi)是可知的:
代碼如下:
x = 5
l = []
for i in range(1000):
l.append(x*i)
該段代碼的優(yōu)化版本不需要用“x 變量/對象的內(nèi)容”乘每個(gè) i,與之相比,簡單地用 5 乘以每個(gè) i 所用的開銷較少,省略了查找/間接引用這一步。
除為小型操作創(chuàng)建特定于 i386 的代碼之外,Psyco 還高速緩存這個(gè)已編譯的機(jī)器碼以備今后重用。如果 Psyco 能夠識別出特定的操作和早先所執(zhí)行的(“專門化的”)操作一樣,那么,它就能依靠這個(gè)高速緩存的代碼而不需要再次編譯代碼段。這樣就節(jié)省了一些時(shí)間。
但是,Psyco 中真正省時(shí)的原因在于 Psyco 將操作分成三個(gè)不同的級別。對于 Psyco,有“運(yùn)行時(shí)”、“編譯時(shí)”和“虛擬時(shí)”變量。Psyco 根據(jù)需要提高和降低變量的級別。運(yùn)行時(shí)變量只是常規(guī) Python 解釋器處理的原始字節(jié)碼和對象結(jié)構(gòu)。一旦 Psyco 將操作編譯成機(jī)器碼,那么編譯時(shí)變量就會在機(jī)器寄存器和可直接訪問的內(nèi)存位置中表示。
最有意思的級別是虛擬時(shí)變量。在內(nèi)部,一個(gè) Python 變量就是一個(gè)有許多成員組成的完整結(jié)構(gòu) - 即使當(dāng)對象只代表一個(gè)整數(shù)時(shí)也是如此。Psyco 虛擬時(shí)變量代表了需要時(shí)可能會被構(gòu)建的 Python 對象,但是這些對象的詳細(xì)信息在它們成為 Python 對象之前是被忽略的。例如,考慮如下賦值:
x = 15 * (14 + (13 - (12 / 11)))
標(biāo)準(zhǔn)的 Python 會構(gòu)建和破壞許多對象以計(jì)算這個(gè)值。構(gòu)建一個(gè)完整的整數(shù)對象以保存 (12/11) 這個(gè)值;然后從臨時(shí)對象的結(jié)構(gòu)中“拉”出一個(gè)值并用它計(jì)算新的臨時(shí)對象 (13-PyInt)。而 Psyco 跳過這些對象,只計(jì)算這些值,因?yàn)樗馈叭绻枰?,可以從值?chuàng)建一個(gè)對象。
使用 Psyco
解釋 Psyco 相對比較困難,但是使用 Psyco 就非常容易了。基本上,其全部內(nèi)容就是告訴 Psyco 模塊哪個(gè)函數(shù)/方法要“專門化”。任何 Python 函數(shù)和類本身的代碼都不需進(jìn)行更改。
有幾種方法可以指定 Psyco 應(yīng)該做什么?!矮C槍(shotgun)”方法使得隨處都可使用 Psyco 即時(shí)操作。要做到這點(diǎn),把下列行置于模塊頂端:
代碼如下:
import psyco ; psyco.jit()
from psyco.classes import *
第一行告訴 Psyco 對所有全局函數(shù)“發(fā)揮其魔力”。第二行(在 Python 2.2 及以上版本中)告訴 Psyco 對類方法執(zhí)行相同的操作。為了更精確地確定 Psyco 的行為,可以使用下列命令:
psyco.bind(somefunc) # or method, class
newname = psyco.proxy(func)
第二種形式把 func 作為標(biāo)準(zhǔn)的 Python 函數(shù),但是優(yōu)化了涉及 newname 的調(diào)用。除了測試和調(diào)試之外的幾乎所有的情況下,您都將使用 psyco.bind() 形式。
Psyco 的性能
盡管 Psyco 如此神奇,使用它仍然需要一點(diǎn)思考和測試。主要是要明白 Psyco 對于處理多次循環(huán)的塊是很有用的,而且它知道如何優(yōu)化涉及整數(shù)和浮點(diǎn)數(shù)的操作。對于非循環(huán)函數(shù)和其它類型對象的操作,Psyco 多半只會增加其分析和內(nèi)部編譯的開銷。而且,對于含有大量函數(shù)和類的應(yīng)用程序來說,在整個(gè)應(yīng)用程序范圍啟用 Psyco,會在機(jī)器碼編譯和用于這一高速緩存的內(nèi)存使用方面增加大量的負(fù)擔(dān)。有選擇性地綁定那些可以從 Psyco 的優(yōu)化中獲得最大收益的函數(shù),這樣會好得多。
我以十分幼稚的方式開始了我的測試過程。我僅僅考慮了我近來運(yùn)行的、但還未考慮加速的應(yīng)用程序。想到的第一個(gè)示例是用來將我即將出版的書稿(Text Processing in Python)轉(zhuǎn)換成 LaTeX 格式的文本操作程序。該應(yīng)用程序使用了一些字符串方法、一些正則表達(dá)式和一些主要由正則表達(dá)式和字符串匹配所驅(qū)動的程序邏輯。實(shí)際上將它用作 Psyco 的測試候選是很糟的選擇,但是我還是使用了,就這么開始了。
第一遍測試中,我所做的就是將 psyco.jit() 添加到腳本頂端。這做起來一點(diǎn)都不費(fèi)力。遺憾的是,結(jié)果(意料當(dāng)中)很令人失望。原先腳本運(yùn)行要花費(fèi) 8.5 秒,經(jīng)過 Psyco 的“加速”后它大概要運(yùn)行 12 秒。真差勁!我猜測大概是即時(shí)編譯所需的啟動開銷拖累了運(yùn)行時(shí)間。因此接下來我試著處理一個(gè)更大的輸入文件(由原來那個(gè)輸入文件的多個(gè)副本組成)。這次獲得了小小的成功,將運(yùn)行時(shí)間從 120 秒左右減到了 110 秒。幾次運(yùn)行中的加速效果比較一致,但是效果都不顯著。
本處理候選項(xiàng)的第二遍測試中。我只添加了 psyco.bind(main) 這一行,而不是添加一個(gè)總的 psyco.jit() 調(diào)用,因?yàn)?main() 函數(shù)確實(shí)要循環(huán)多次(但是僅利用了最少的整數(shù)運(yùn)算)。這里的結(jié)果名義上要比前面好。這種方法將正常的運(yùn)行時(shí)間削減了十分之幾秒,在較大的輸入版本的情況下削減了數(shù)秒鐘。但是仍然沒有引入矚目的結(jié)果發(fā)生(但也沒產(chǎn)生什么害處)。
為進(jìn)行更恰當(dāng)?shù)?Psyco 測試,我搜尋出我在以前的文章里編寫的一些神經(jīng)網(wǎng)絡(luò)代碼(請參閱“參考資料”)。這個(gè)“代碼識別器(code_recognizer)”應(yīng)用程序可以經(jīng)“訓(xùn)練”用于識別不同編程語言編寫的不同 ASCII 值的可能分布情況。類似于這樣的東西可能在猜測文件類型方面(比方說丟失的網(wǎng)絡(luò)信息包)將很有用;但是,關(guān)于“訓(xùn)練”些什么,代碼實(shí)際上完全是通用的 - 它能很容易地學(xué)會識別面孔、聲音或潮汐模式。任何情況下,“代碼識別器”都基于 Python 庫 bpnn,Psyco 4.0 分發(fā)版也包含(以修正的形式)了該庫作為測試用例。在本文中,對“代碼識別器”要重點(diǎn)了解它做了許多浮點(diǎn)運(yùn)算循環(huán)并花費(fèi)了很長的運(yùn)行時(shí)間。這里我們已經(jīng)有了一個(gè)能用于 Psyco 測試的好的候選用例。
使用了一段時(shí)間后,我建立了有關(guān) Psyco 用法的一些詳細(xì)信息。對于這種只有少量類和函數(shù)的應(yīng)用程序,使用即時(shí)綁定還是目標(biāo)綁定沒有太大區(qū)別。但最佳的結(jié)果是,通過有選擇性地綁定最優(yōu)化類,仍可得到幾個(gè)百分點(diǎn)的改進(jìn)。然而,更值得注意的是要理解 Psyco 綁定的作用域,這一點(diǎn)很重要。
code_recognizer.py 腳本包括類似于下面的這些行:
從 bpnn 導(dǎo)入 NN
class NN2(NN):
# customized output methods, math core inherited
也就是說,從 Psyco 的觀點(diǎn)來看,有趣的事情在類 bpnn.NN 之中。把 psyco.jit() 或 psyco.bind(NN2) 添加到 code_recognizer.py 腳本中起不了什么作用。要使 Psyco 進(jìn)行期望的優(yōu)化,需要將 psyco.bind(NN) 添加到 code_recognizer.py 或者將 psyco.jit() 添加到 bpnn.py。與您可能假設(shè)的情況相反,即時(shí)優(yōu)化不在創(chuàng)建實(shí)例時(shí)或方法運(yùn)行時(shí)發(fā)生,而是在定義類的作用域內(nèi)發(fā)生。另外,綁定派生類不會專門化其從其它地方繼承的方法。
一旦找到適當(dāng)?shù)?Psyco 綁定的細(xì)微的詳細(xì)信息,那么加速效果是相當(dāng)明顯的。使用參考文章中提供的相同測試用例和訓(xùn)練方法(500 個(gè)訓(xùn)練模式,1000 個(gè)訓(xùn)練迭代),神經(jīng)網(wǎng)絡(luò)訓(xùn)練時(shí)間從 2000 秒左右減到了 600 秒左右 - 提速了 3 倍多。將迭代次數(shù)降到 10,加速的倍數(shù)也成比例降低(但對神經(jīng)網(wǎng)絡(luò)的識別能力無效),迭代的中間數(shù)值也會如此變化。
我發(fā)現(xiàn)使用兩行新代碼就能將運(yùn)行時(shí)間從超過半小時(shí)減到 10 分鐘左右,效果非常顯著。這種加速仍可能比 C 編寫的類似應(yīng)用程序的速度慢,而且它肯定比幾個(gè)獨(dú)立的 Psyco 測試用例所反映出的 100 倍加速要慢。但是這種應(yīng)用程序是相當(dāng)“真實(shí)的”,而且在許多環(huán)境中這些改進(jìn)已經(jīng)是夠顯著的了。
更多信息請查看IT技術(shù)專欄