歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux綜合 >> Linux資訊 >> 更多Linux

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

  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




Copyright © Linux教程網 All Rights Reserved