本節書摘來自異步社群《python高性能程式設計》一書中的第2章,第2.8節,作者[美] 戈雷利克 (micha gorelick),胡世傑,徐旭彬 譯,更多章節内容可以通路雲栖社群“異步社群”公衆号檢視。
根據ian的觀點,robert kern的line_profiler是調查python的cpu密集型性能問題最強大的工具。它可以對函數進行逐行分析,你應該先用cprofile找到需要分析的函數,然後用line_profiler對函數進行分析。
當你修改你的代碼時,值得列印出這個工具的輸出以及代碼的版本,這樣你就擁有一個代碼變化(無論有沒有用)的記錄,讓你可以随時查閱。當你在進行逐行改變時,不要依賴你的記憶。
輸入指令<code>pip install line_profiler</code>來安裝line_profiler。
用修飾器(@profile)标記選中的函數。用kernprof.py腳本運作你的代碼,被選函數每一行花費的cpu時間以及其他資訊就會被記錄下來。
運作時參數-l代表逐行分析而不是逐函數分析,-v用于顯示輸出。沒有-v,你會得到一個.lprof的輸出檔案,回頭你可以用line_profiler子產品對其進行分析。例2-6中,我們會完整運作一遍我們的cpu密集型函數。
例2-6 運作kernprof逐行分析被修飾函數的cpu開銷
引入kernprof.py導緻了額外的運作時間。本例的calculate_z_serial_purepython花費了100秒,遠高于使用print語句的13秒和cprofile的19秒。獲得的好處則是我們現在得到了一個函數内部每一行花費時間的分析結果。
%time列最有用——我們可以看到36%的時間花在了while測試上。不過我們不知道是第一條語句(abs(z) < 2)還是第二條語句(n < maxiter)更花時間。循環内,我們可以看到更新z也頗花時間。甚至n += 1都很貴!每次循環時,python的動态查詢機制都在工作,即使每次循環中我們使用的變量都是同樣的類型——在這一點上,編譯和類型指定(第7章)可以給我們帶來巨大的好處。建立output清單以及第20行上的更新相對整個while循環來說相當便宜。
對while語句更進一步的分析明顯就是将兩個判斷拆開。python社群中有一些讨論關于是否需要重寫.pyc檔案中對于一行語句中多個部分的具體資訊,但目前還沒有一個工具提供比line_profiler更細粒度的分析。
在例2-7中,我們将while語句分拆成多個語句。這一額外的複雜度會增加函數的運作時間,因為我們有了更多行代碼需要執行,但它可能可以幫助我們了解這部分代碼的開銷。
例2-7 将組合式while語句拆成單個語句來記錄每一部分的開銷
這個版本花了184秒執行,而之前的僅100秒。其他因素确實讓分析變得更複雜。本例中每一條額外語句都執行了34219980次,拖慢了代碼。如果不是通過kernprof.py調查了每行的影響,我們可能會在缺乏證據的情況下得出是其他原因導緻了變慢的結論。
此時有必要回到之前的timeit技術來測試每個單獨表達式的開銷:
從這一簡單分析上來看,對n的邏輯測試的速度幾乎是abs函數調用的兩倍。既然python語句的評估次序是從左到右且支援短路,那麼我們應該将最便宜的測試放在左邊。每301次測試就有1次n < maxiter的值為false,這樣python就不必評估and操作符右邊的語句了。
在評估前我們永遠無法知道abs(z) < 2的值何時為false,而我們之前對複數平面的觀察告訴我們300次疊代中大約10%的可能是true。如果我們想要更進一步了解這段代碼的時間複雜度,有必要繼續進行數值分析。不過在目前的情況下,我們隻是想要看看有沒有快速提高的機會。
我們可以做一個新的假設聲明,“通過交換while語句的次序,我們會獲得一個可靠的速度提升。”我們可以用kernprof.py測試這個假設,但是其額外的開銷 可能會給我們的結果帶來太多噪聲。是以我們用一個之前版本的代碼,測試比較while abs(z) < 2 and n < maxiter:和while n < maxiter and abs(z) < 2:之間的差別。
結果顯示出大約0.4秒的穩定提升。這一結果顯然很無足輕重且局限性太強,使用另一個更合适的方法(如換用第7章描述的cython或pypy)來解決問題會帶來更高的收益。
我們對自己的結果有信心,是因為:
我們聲明的假設易于測試。
我們對代碼的改動僅局限于假設的測試(永遠不要一次測試兩件 事!)。
我們收集了足夠的證據支援我們的結論。
為了保持完整性,我們可以在包含了我們優化的兩個主要函數上最後運作一次kernprof.py來确認我們代碼整體的複雜度。例2-8交換了第17行while測試的語句,我們可以看到原來占用的36.1%的執行時間現在僅占用35.9%(這一結果在多次運作中穩定存在)。
例2-8 交換while語句的次序提升測試的速度
和預期的一樣,我們可以看例2-9的輸出中,calculate_z_serial_purepython占用了其父函數97%的時間。建立清單的步驟相對來說無足輕重。
例2-9 逐行測試設定階段的開銷