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