我們應該忽略一些微小的效率提升,幾乎在 97% 的情況下,都是如此:過早的優化是萬惡之源。—— donald knuth
如果不先想想knuth的這句名言,就開始進行優化工作,是不明智的。然而,有時你為了獲得某些特性不假思索就寫下了o(n^2) 這樣的代碼,雖然你很快就忘記它們了,它們卻可能反咬你一口,給你帶來麻煩:本文就是為這種情況而準備的。
本文會介紹用于快速分析python程式的一些有用工具和模式。主要目标很簡單:盡快發現問題,修複問題,并确認修複是行之有效的。
在教程開始前,要先寫一個簡單的概要測試來示範延遲。你可能需要引入一些最小資料集來重制可觀的延遲。通常一或兩秒的運作時間,已經足夠在發現問題時,讓你做出改進了。
此外,進行一些基礎測試來確定你的優化不會修改緩慢代碼的行為也是有必要的。在分析和調整時,你也可以多次運作這些測試,作為基準。
那麼現在,我們來看第一個分析工具。
計時器是簡單、靈活的記錄執行時間的方法。你可以把它放到任何地方,并且幾乎沒有副作用。自己建立計時器非常簡單,并且可以根據你的喜好定制化。例如,一個簡單的計時器可以這麼寫:
當然,你可以用上下文管理器來增強它的功能,添加一些檢查點或其他小功能:
有了計時器,你還需要進行一些“挖掘”工作。 封裝一些更為進階的函數,然後确定問題根源之所在,進而深入可疑的函數,不斷重複。當你發現運作特别緩慢的代碼之後,修複它,然後進行測試以确認修複成功。
計時器的優點:容易了解和實施,也非常容易在修改前後進行對比,對于很多語言都适用。
計時器的缺點:有時候,對于非常複雜的代碼庫而已太過簡單,你可能會花更多的時間建立、替換樣闆代碼,而不是修複問題!
内建分析器就好像大型槍械。雖然非常強大,但是有點不太好用,有時,解釋和操作起來比較困難。
在上面代碼中,控制台列印的内容如下:
如你所見,它給出了不同函數調用的詳細資料。但是,它遺漏了一項關鍵資訊:是什麼原因,導緻函數運作如此緩慢?
然而,這對于基礎分析來說是個好的開端。有時,能夠幫你盡快找到解決方案。我經常在開始調試過程時,把它作為基本測試,然後再深入測試某個不是運作緩慢,就是調用頻繁的特定函數。
内建分析器的優點:沒有外部依賴,運作非常快。對于快速的概要測試非常有用。
内建分析器的缺點:資訊相對有限,需要進一步的調試;報告不太直覺,尤其是對于複雜的代碼庫。
如果内建分析器是大型槍械,line profiler就好比是離子炮。它非常的重量級且強大,使用起來也非常有趣。
如果運作上面的代碼,就會看到以下的報告:
如你所見,這是一個非常詳細的報告,能讓你完全洞悉代碼的運作情況。和内建的cprofiler不同,它能分析核心語言特性的耗時,比如循環或導入,并且給出不同代碼行的耗時累計值。
這些細節能讓我們更容易了解函數内部原理。 此外,如果需要研究第三方庫,你可以将其導入,直接輸到裝飾器中。
提示:将測試函數封裝為裝飾器,再将問題函數作為參數傳進去就好了!
line profiler 的優點:有非常直接和詳細的報告。能夠追蹤第三方庫裡的函數。
line profiler 的缺點:因為系統開銷巨大,會比實際執行時間慢一個數量級,是以不要用它進行基準測試。同時,它是外部工具。
你應該使用簡單的工具(比如計時器或内建分析器)對測試用例(特别是那些你非常熟悉的代碼)進行基本檢查,然後使用更慢但更加細緻的工具,比如 <code>line_profiler</code>,深入檢查函數内部。
十有八九,你會發現一個愚蠢的錯誤,比如在循環内重複調用,或是使用了錯誤的資料結構,消耗了90%的函數執行時間。在進行快速(且令人滿意的)調整之後,問題就能得到解決。
如果你仍然覺得程式運作太過緩慢,然後開始進行對比屬性通路(ttribute accessing)方法,或調整相等檢查(equality checking)方法等晦澀的調整,你可能已經适得其反了。你應該考慮如下方法:
1.忍受緩慢或者預先計算/緩存
2.重新思考整個實施方法
3.使用更多的優化資料結構(通過 numpy,pandas等)
注意,優化代碼會帶來有罪惡感的快樂!尋找加速python的合理方法很有趣,但是不要因為加速,破壞了本身的邏輯。易讀的代碼比運作速度更重要。實施緩存,往往是最簡單的解決方法。
教程到此為止,希望你今後的python性能分析能夠如魚得水!