歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Unix知識 >> 關於Unix >> 用Psyco讓Python運行得像C一樣快

用Psyco讓Python運行得像C一樣快

日期:2017/3/6 15:51:14   编辑:關於Unix
Python 的設計在很多方面都類似於 Java 的設計。兩者都利用了解釋專門的偽編譯字節碼的虛擬機。JVM 比 Python 更高級的一個方面在於優化了字節碼的執行。Psyco,一種 Python 專用編譯器,幫助平衡了這一競爭。Psyco 現在是個外部模塊,但是在將來的某一天它
  Python 的設計在很多方面都類似於 Java 的設計。兩者都利用了解釋專門的偽編譯字節碼的虛擬機。JVM 比 Python 更高級的一個方面在於優化了字節碼的執行。Psyco,一種 Python 專用編譯器,幫助平衡了這一競爭。Psyco 現在是個外部模塊,但是在將來的某一天它可能會包括到 Python 本身中去。只需極少量的額外編程,通常就可以使用 Psyco 將 Python 代碼的速度提高好幾個數量級。在本文中,David Mertz 研究了 Psyco 是什麼,並在一些應用程序中對它進行了測試。
  
  Python 對於您想讓它做的事來說通常夠快了。編程新手對於類似 Python 這樣的解釋型/字節編譯型語言,將 90% 的關注點集中在執行速度方面,是相當幼稚的。在最新的硬件上,大多數非優化的 Python 程序運行的速度和所需要達到的速度一樣快,而且,花費額外的編程工作以使應用程序運行得更快實在沒什麼意義。
  
  因此,在本文,我只對其它的百分之十感興趣。有時,Python 程序(或用其它語言編寫的程序)也會運行得極其緩慢。不同的目的所需要的改進差異很大;提高只運行幾毫秒的任務的性能極少能引人注目,但是加快那些需運行幾分鐘、幾小時、幾天甚至幾星期的任務的速度通常是很值得的。而且,應該注意到並不是所有任務運行緩慢的原因都是由 CPU 引起的。例如,如果完成一個數據庫查詢要花費幾個小時,那麼處理結果數據集要花費一分鐘還是兩分鐘就沒什麼差別了。本文同樣不討論與 I/O 有關的問題。
  
  有很多方法可以加速 Python 程序。每個程序員都應當想到的第一種技術就是改進所使用的算法和數據結構。對低效算法步驟進行細微的優化是徒勞無益的事情。例如,如果當前技術的復雜性等級是 O(n**2),那麼將這些步驟加速 10 倍遠不及尋找 O(n) 替代品來得有用。即使在考慮用匯編語言重寫算法這種極端情況時,這種思想也都適用:Python 中正確的算法通常會比手工調優的匯編語言中的錯誤算法快得多。
  
  第二種您應當首先考慮的技術是概要分析您的 Python 應用程序,要著眼於將關鍵部分重寫成 C 擴展模塊。使用像 SWIG 這樣的擴展封裝器(請參閱參考資料),可以創建 C 擴展,它將程序中最耗時元素作為 C 代碼執行。以這種方式擴展 Python 相對簡單,但要花些時間學習(並且需要了解 C 的知識)。您經常會發現執行 Python 應用程序所花費的時間絕大部分只是花在了幾個函數上,因此,采用這種擴展可能會有很可觀的“成果”。
  
  第三種技術建立在第二種技術的基礎之上。Greg Ewing 已經創建了名叫 Pyrex 的語言,該語言融合了 Python 和 C。特別地,要使用 Pyrex,需要用類似 Python 的語言編寫函數,這種語言將類型聲明添加到所選變量中。Pyrex(工具)將“.pyx”文件處理成“.c”擴展名的文件。一旦用 C 編譯器進行了編譯,就可以將這些 Pyrex(語言)模塊導入常規的 Python 應用程序並使用。由於 Pyrex 使用的語法和 Python 本身的語法(包括循環、分支和異常語句、賦值方式、塊縮進等等)幾乎一樣,因此 Pyrex 程序員不需要學會用 C 去編寫擴展。而且,與直接用 C 編寫擴展相比,Pyrex 允許在同一代碼中更無縫地混合 C 級別的變量和 Python 級別的變量(對象)。
  
  最後一種技術就是本文的主題。擴展模塊 Psyco 可以插入 Python 解釋器的內部,而且可以有選擇性地用優化的機器代碼去替換部分 Python 的解釋型字節碼。和所描述的其它技術不同,Psyco 是嚴格地在 Python 運行時進行操作的。也就是說,Python 源代碼是通過 python 命令編譯成字節碼的,所用的方式和以前完全相同(除了為調用 Psyco 而添加的幾個 import 語句和函數調用)。但是當 Python 解釋器運行應用程序時,Psyco 會不時地檢查,看是否能用一些專門的機器代碼去替換常規的 Python 字節碼操作。這種專門的編譯和 Java 即時編譯器所進行的操作非常類似(一般地說,至少是這樣),並且是特定於體系結構的。到現在為止,Psyco 只可用於 i386 CPU 體系結構。Psyco 的妙處在於可以使用您一直在編寫的 Python 代碼(完全一樣!),卻可以讓它運行得更快。
  
  Psyco 是如何工作的
  要完全理解 Psyco,您可能需要很好地掌握 Python 解釋器的 eval_frame() 函數和 i386 匯編語言。遺憾的是,我自己不能對其中任何一項發表專家性的意見 - 但是我想我可以大致不差地概述 Psyco。
  
  在常規的 Python 中,eval_frame() 函數是 Python 解釋器的內循環。eval_frame() 函數主要察看執行上下文中的當前字節碼,並將控制向外切換到一個適合實現該字節碼的函數。支持函數將做什麼的具體細節通常取決於保存在內存中的各種 Python 對象的狀態。簡單點說,添加 Python 對象“2”和“3”和添加對象“5”和“6”會產生不同的結果,但是這兩個操作都以類似的方式分派。
  
  Psyco 用復合求值單元替代 eval_frame() 函數。Psyco 有幾種方法可以用來改進 Python 所進行的操作。首先,Psyco 將操作編譯成有點優化的機器碼;由於機器碼需要完成的工作和 Python 的分派函數所要做的事一樣,所以其本身只有些許改進。而且,Psyco 編譯中的“專門的”內容不僅僅是對 Python 字節碼的選擇,Psyco 也要對執行上下文中已知的變量值進行專門化。例如,在類似於下面的代碼中,變量 x 在循環持續時間內是可知的:
  
  x = 5
  l = []
  for i in range(1000):
    l.append(x*i)
  
  該段代碼的優化版本不需要用“x 變量/對象的內容”乘每個 i,與之相比,簡單地用 5 乘以每個 i 所用的開銷較少,省略了查找/間接引用這一步。
  
  除為小型操作創建特定於 i386 的代碼之外,Psyco 還高速緩存這個已編譯的機器碼以備今後重用。如果 Psyco 能夠識別出特定的操作和早先所執行的(“專門化的”)操作一樣,那麼,它就能依靠這個高速緩存的代碼而不需要再次編譯代碼段。這樣就節省了一些時間。
  
  但是,Psyco 中真正省時的原因在於 Psyco 將操作分成三個不同的級別。對於 Psyco,有“運行時”、“編譯時”和“虛擬時”變量。Psyco 根據需要提高和降低變量的級別。運行時變量只是常規 Python 解釋器處理的原始字節碼和對象結構。一旦 Psyco 將操作編譯成機器碼,那麼編譯時變量就會在機器寄存器和可直接訪問的內存位置中表示。
  
  最有意思的級別是虛擬時變量。在內部,一個 Python 變量就是一個有許多成員組成的完整結構 - 即使當對象只代表一個整數時也是如此。Psyco 虛擬時變量代表了需要時可能會被構建的 Python 對象,但是這些對象的詳細信息在它們成為 Python 對象之前是被忽略的。例如,考慮如下賦值:
  
  x = 15 * (14 + (13 - (12 / 11)))
  
  標准的 Python 會構建和破壞許多對象以計算這個值。構建一個完整的整數對象以保存 (12/11) 這個值;然後從臨時對象的結構中“拉”出一個值並用它計算新的臨時對象 (13-PyInt)。而 Psyco 跳過這些對象,只計算這些值,因為它知道“如果需要”,可以從值創建一個對象。
  
  使用 Psyco
  解釋 Psyco 相對比較困難,但是使用 Psyco 就非常容易了。基本上,其全部內容就是告訴 Psyco 模塊哪個函數/方法要“專門化”。任何 Python 函數和類本身的代碼都不需進行更改。
  
  有幾種方法可以指定 Psyco 應該做什麼。“獵槍(shotgun)”方法使得隨處都可使用 Psyco 即時操作。要做到這點,把下列行置於模塊頂端:
  
  import psyco ; psyco.jit()
  from psyco.classes import *
  
  第一行告訴 Psyco 對所有全局函數“發揮其魔力”。第二行(在 Python 2.2 及以上版本中)告訴 Psyco 對類方法執行相同的操作。為了更精確地確定 Psyco 的行為,可以使用下列命令:
  
  psyco.bind(somefunc)     # or method, class
  newname = psyco.proxy(func)
  
  第二種形式把 func 作為標准的 Python 函數,但是優化了涉及 newname 的調用。除了測試和調試之外的幾乎所有的情況下,您都將使用 psyco.bind() 形式。
  
  Psyco 的性能
  盡管 Psyco 如此神奇,使用它仍然需要一點思考和測試。主要是要明白 Psyco 對於處理多次循環的塊是很有用的,而且它知道如何優化涉及整數和浮點數的操作。對於非循環函數和其它類型對象的操作,Psyco 多半只會增加其分析和內部編譯的開銷。而且,對於含有大量函數和類的應用程序來說,在整個應用程序范圍啟用 Psyco,會在機器碼編譯和用於這一高速緩存的內存使用方面增加大量的負擔。有選擇性地綁定那些可以從 Psyco 的優化中獲得最大收益的函數,這樣會好得多。
  
  我以十分幼稚的方式開始了我的測試過程。我僅僅考慮了我近來運行的、但還未考慮加速的應用程序。想到的第一個示例是用來將我即將出版的書稿(Text Processing in Python)轉換成 LaTeX 格式的文本操作程序。該應用程序使用了一些字符串方法、一些正則表達式和一些主要由正則表達式和字符串匹配所驅動的程序邏輯。實際上將它用作 Psyco 的測試候選是很糟的選擇,但是我還是使用了,就這麼開始了。
  
  第一遍測試中,我所做的就是將 psyco.jit() 添加到腳本頂端。這做起來一點都不費力。遺憾的是,結果(意料當中)很令人失望。原先腳本運行要花費 8.5 秒,經過 Psyco 的“加速”後它大概要運行 12 秒。真差勁!我猜測大概是即時編譯所需的啟動開銷拖累了運行時間。因此接下來我試著處理一個更大的輸入文件(由原來那個輸入文件的多個副本組成)。這次獲得了小小的成功,將運行時間從 120 秒左右減到了 110 秒。幾次運行中的加速效果比較一致,但是效果都不顯著。
  
  文本處理候選項的第二遍測試中。我只添加了 psyco.bind(main) 這一行,而不是添加一個總的 psyco.jit() 調用,因為 main() 函數確實要循環多次(但是僅利用了最少的整數運算)。這裡的結果名義上要比前面好。這種方法將正常的運行時間削減了十分之幾秒,在較大的輸入版本的情況下削減了數秒鐘。但是仍然沒有引入矚目的結果發生(但也沒產生什麼害處)。
  
  為進行更恰當的 Psyco 測試,我搜尋出我在以前的文章裡編寫的一些神經網絡代碼(請參閱“參考資料”)。這個“代碼識別器(code_recognizer)”應用程序可以經“訓練”用於識別不同編程語言編寫的不同 ASCII 值的可能分布情況。類似於這樣的東西可能在猜測文件類型方面(比方說丟失的網絡信息包)將很有用;但是,關於“訓練”些什麼,代碼實際上完全是通用的 - 它能很容易地學會識別面孔、聲音或潮汐模式。任何情況下,“代碼識別器”都基於 Python 庫 bpnn,Psyco 4.0 分發版也包含(以修正的形式)了該庫作為測試用例。在本文中,對“代碼識別器”要重點了解它做了許多浮點運算循環並花費了很長的運行時間。這裡我們已經有了一個能用於 Psyco 測試的好的候選用例。
  
  使用了一段時間後,我建立了有關 Psyco 用法的一些詳細信息。對於這種只有少量類和函數的應用程序,使用即時綁定還是目標綁定沒有太大區別。但最佳的結果是,通過有選擇性地綁定最優化類,仍可得到幾個百分點的改進。然而,更值得注意的是要理解 Psyco 綁定的作用域,這一點很重要。
  
  code_recognizer.py 腳本包括類似於下面的這些行:
  
  從 bpnn 導入 NN
  class NN2(NN):
    # customized output methods, math core inherited
  
  也就是說,從 Psyco 的觀點來看,有趣的事情在類 bpnn.NN 之中。把 psyco.jit() 或 psyco.bind(NN2) 添加到 code_recognizer.py 腳本中起不了什麼作用。要使 Psyco 進行期望的優化,需要將 psyco.bind(NN) 添加到 code_recognizer.py 或者將 psyco.jit() 添加到 bpnn.py。與您可能假設的情況相反,即時優化不在創建實例時或方法運行時發生,而是在定義類的作用域內發生。另外,綁定派生類不會專門化其從其它地方繼承的方法。
  
  一旦找到適當的 Psyco 綁定的細微的詳細信息,那麼加速效果是相當明顯的。使用參考文章中提供的相同測試用例和訓練方法(500 個訓練模式,1000 個訓練迭代),神經網絡訓練時間從 2000 秒左右減到了 600 秒左右 - 提速了 3 倍多。將迭代次數降到 10,加速的倍數也成比例降低(但對神經網絡的識別能力無效),迭代的中間數值也會如此變化。
  
  我發現使用兩行新代碼就能將運行時間從超過半小時減到 10 分鐘左右,效果非常顯著。這種加速仍可能比 C 編寫的類似應用程序的速度慢,而且它肯定比幾個獨立的 Psyco 測試用例所反映出的 100 倍加速要慢。但是這種應用程序是相當“真實的”,而且在許多環境中這些改進已經是夠顯著的了。
  
  Psyco 將何去何從?
  Psyco 現在不執行任何類型的內部統計或概要分析,只對生成的機器碼進行最小優化。可能今後的版本會知道如何針對那些能真正最大獲益 Python,並且丟棄為不可優化部分高速緩存的機器碼。另外,也許今後的 Psyco 可能會決定對費力運行的操作進行更廣泛(但也更昂貴)的優化。這種運行時分析可能類似於 Sun 的 HotSpot 技術為 Java 所做的工作。Java 不像 Python,它有類型聲明,但這一事實實際上沒有許多人所設想的那樣重要(但是先前在 Self、Smalltalk、Lisp 和 Scheme 的優化方面所做的工作也說明了這一點)。
  
  若將 Psyco 類型的技術集成到 Python 本身的某個未來版本中去,會多麼令人激動,盡管我懷疑這永遠不會真正發生。添加幾行導入和綁定代碼不需要做很多工作,但卻可以輕易地讓 Python 比以前運行得快得多。我們將看到這一點。

Copyright © Linux教程網 All Rights Reserved