Python 應用剖析工具介紹

OneAPM官方技術部落格發表於2016-05-06

【編者按】本文作者為來自 HumanGeo 的工程師 Davis,主要介紹了用於 Python 應用效能分析的幾個工具。由國內 ITOM 管理平臺 OneAPM 編譯呈現。

HumanGeo,我們廣泛使用 Python 進行程式設計,並且樂趣無窮。用 Python 寫的程式不僅整潔美觀,而且執行速度快得驚人。不論是私底下還是工作中,Python 都是筆者最愛的語言。然而,即便是 Python 這樣美妙的語言,卻也可能出現執行緩慢的情況。幸運的是,有許多不錯的工具,可以幫助我們分析 Python 程式碼,從而保證其執行效率。

當筆者剛開始在 HumanGeo 工作時,就曾遇到過一個執行一次耗時數小時的程式,而筆者的任務,就是找出其效能瓶頸,再儘可能地提高其執行效率。當時,筆者使用了許多工具,包括 cProfilePyCallGraph(原始碼),甚至 PyPy(一個執行快速的 Python 直譯器),以確定最佳的程式優化方案。在本文中,筆者將介紹上述工具(為了保持生產環境中的直譯器一致性,本文將不會介紹 PyPy 工具)的使用方法。甚至即便是最老練的開發者,也可以藉助這些工具進一步優化他們的程式碼。

免責宣告:不要過早地進行優化!有關過早優化的詳細分析請查閱本文

工具

閒話少敘,下面開始介紹分析 Python 程式碼的幾種便捷工具。

cProfile

CPython distribution 自帶兩種分析工具:profilecProfile。兩者使用同樣的 API,按理說執行效果應該差不多。然而,前者的執行時開銷更大,因此,本文將主要介紹 cProfile

藉助 cProfile,可以輕鬆實現對程式碼的深入分析,並且瞭解程式碼的哪些部分亟待提升。檢視下面的緩慢程式碼例項:

--> % cat slow.py
import time

def main():    
  sum = 0    
  for i in range(10):        
      sum += expensive(i // 2)    
  return sum

def expensive(t):    
   time.sleep(t)    
   return t

if __name__ == '__main__':
    print(main())

在上面的程式碼中,筆者通過呼叫 time.sleep 方法,模擬一個執行時間很長的程式,並假定執行結果很重要。接下來,對這段程式碼進行分析,結果如下:

--> % python -m cProfile slow.py
20
         34 function calls in 20.030 seconds

   Ordered by: standard name   
 ncalls  tottime  percall  cumtime  percall filename:lineno(function)        
 1    0.000    0.000    0.000    0.000 __future__.py:48(<module>)        
 1    0.000    0.000    0.000    0.000 __future__.py:74(_Feature)        
 7    0.000    0.000    0.000    0.000 __future__.py:75(__init__)       
 10    0.000    0.000   20.027    2.003 slow.py:11(expensive)        
 1    0.002    0.002   20.030   20.030 slow.py:2(<module>)        
 1    0.000    0.000   20.027   20.027 slow.py:5(main)        
 1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}        
 1    0.000    0.000    0.000    0.000 {print}        
 1    0.000    0.000    0.000    0.000 {range}       
 10   20.027    2.003   20.027    2.003 {time.sleep}

我們發現,分析結果相當瑣碎。其實,可以用更有益的方式組織分析結果。在上例中,呼叫列表是按照字母順序排列的,這對我們並無價值。筆者更願意看到按照呼叫次數或累計執行時間排列的呼叫情況。幸運的是,通過 -s 引數就能實現這一點。我們馬上就能看到存在問題的程式碼段了!

--> % python -m cProfile -s calls slow.py
20
         34 function calls in 20.028 seconds

   Ordered by: call count   

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)       
   10    0.000    0.000   20.025    2.003 slow.py:11(expensive)       
   10   20.025    2.003   20.025    2.003 {time.sleep}        
   7    0.000    0.000    0.000    0.000 __future__.py:75(__init__)        
   1    0.000    0.000   20.026   20.026 slow.py:5(main)        
   1    0.000    0.000    0.000    0.000 __future__.py:74(_Feature)        
   1    0.000    0.000    0.000    0.000 {print}        
   1    0.000    0.000    0.000    0.000 __future__.py:48(<module>)        
   1    0.003    0.003   20.028   20.028 slow.py:2(<module>)        
   1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}        
   1    0.000    0.000    0.000    0.000 {range}

果然!我們發現,存在問題的程式碼就在 expensive 函式當中。該函式在執行結束之前呼叫了多次 time.sleep 方法,因此導致了程式的速度下降。

-s引數的有效取值列表可以在此 Python 文件中找到。如果你想將分析結果儲存到一個檔案中,記得使用輸出選項 -o

基本功能介紹完畢之後,讓我們來看看使用分析工具查詢問題程式碼的其他方法。

PyCallGraph

PyCallGraph 可以看做是 cProfile 的視覺化擴充套件工具。藉助該工具,我們可以通過出色的 Graphviz 圖片瞭解程式碼執行的路徑。PyCallGraph 並未包含在標準的 Python 安裝包內,因此,需要通過如下語句,進行簡單的安裝:

-> % pip install pycallgraph

通過下面的指令,就能執行圖形化應用:

-> % pycallgraph graphviz -- python slow.py

執行完畢之後,在執行指令碼的目錄下會出現一張 pycallgraph.png 圖片檔案。同時,還應該得到相似的分析結果(如果你之前已經用 cProfile 分析過了)。結果中的資料應該與 cProfile 提供的結果一致。不過,PyCallGraph 的優點在於,它能展示被呼叫函式相互間的關係。

讓我們來看看圖片到底長什麼樣:

Python 應用剖析工具介紹

這多方便啊!圖片顯示了程式的執行路徑,告訴我們程式經歷過的每個函式、模組以及檔案,還帶有執行時間與呼叫次數等資訊。如果在龐大的應用中執行該分析工具,會得到一張巨大的圖片。但是,根據顏色的差別,我們仍能輕易找到存在問題的程式碼塊。下面是 PyCallGraph 文件中提供的一張圖片,展示了一段複雜的正規表示式呼叫中程式碼的執行路徑:

Python 應用剖析工具介紹

點此獲取此圖分析的原始碼

這些資訊有什麼用?

一旦我們確定了導致問題程式碼的根源,就可以選擇合適的解決方案優化程式碼,為其提速。下面,讓我們根據特定的情況,探討一些緩慢程式碼可行的解決方案。

I/O

如果你發現自己的程式碼嚴重依賴於輸入/輸出,譬如,需要傳送很多 Web 請求,那麼,Python 的標準執行緒模組或許就能幫你解決該問題。由於 CPython 的全域性鎖機制(Global Interpreter Lock,GIL)不允許為程式碼中心任務同時使用多個核,非 I/O 相關的執行緒並不適合用 Python 實現。

正規表示式

人們都說,一旦你決定用正規表示式解決某個問題,你就有兩個問題要解決了。正規表示式真的很難用對,而且難以維護。關於這一點,筆者可以寫一篇長篇大論進行闡述。(但是,我不會寫的:)。正規表示式真的不簡單,我相信有很多博文已經做了詳盡的闡述。)不過,在此,筆者將介紹幾個有用的技巧:

  1. 避免使用 .*,貪婪的匹配所有運算子執行起來非常慢,儘可能使用字元類才是更好的選擇。
  2. 避免使用正規表示式!其實,許多正規表示式都可以用簡單的字串方法替代,比如 str.startswithstr.endswith 方法。閱讀 str 文件可以找到更多有用的資訊。
  3. 多使用 re.VERBOSE!Python 的正規表示式引擎非常強大,超級有用,一定要好好利用!

以上是有關正規表示式筆者想說的全部內容。如果你想要更多資訊,相信網路上還有很多好的文章。

Python 程式碼

以筆者之前剖析過的程式碼為例,我們的 Python 函式會執行成千上萬次以找出英文詞的詞根。該函式最迷人的地方在於,其進行的操作很容易快取。儲存函式的執行結果之後,程式碼的執行速度提升了整整十倍。而在 Python 中建立快取是輕而易舉的事情:

from functools import wraps
def memoize(f):
    cache = {}    
    @wraps(f)    
    def inner(arg):       
       if arg not in cache:
            cache[arg] = f(arg)        
       return cache[arg]   
     return inner

該技術名為記憶(memoization),在具體實現時會執行為裝飾器,可輕易應用在 Python 函式中,如下所示:

import time
@memoize
def slow(you):
    time.sleep(3)
    print("Hello after 3 seconds, {}!".format(you))    
    return 3

現在,如果我們多次執行該函式,執行結果就會立即出現:

>>> slow("Davis")
Hello after 3 seconds, Davis!
3
>>> slow("Davis")
3
>>> slow("Visitor")
Hello after 3 seconds, Visitor!
3
>>> slow("Visitor")
3

對於該專案來說,這是極大的速度提升。而且程式碼執行起來也沒有出現故障。

免責宣告:請確保該方法只用於 pure 函式!如果將記憶(memoization)用於帶有副作用(譬如:I/O)的函式,快取可能無法達到預期的效果。

其他情況

如果你的程式碼無法使用記憶(memoization)技巧,你的演算法也不像 O(n!) 這樣瘋狂,或者程式碼的剖析結果也沒有引人注意的地方,這可能說明你的程式碼並不存在顯著的問題。這時候,你可以嘗試一下別的執行環境或語言。PyPy 就是一個好的選擇,你可能還要將演算法用C語言擴充套件方法重寫一下。幸運的是,筆者之前的專案並未走到這一步,但是這仍是很好的排錯方案。

結論

剖析程式碼可以幫助你理解專案的執行流程、找出潛在的問題程式碼,以及作為開發者該如何提升程式執行速度。Python 剖析工具不但功能強大,簡單易用,而且足夠深入以快速找出問題根源。雖然 Python 並不是以快速著稱的語言,但這並不意味著你的程式碼應該拖拖拉拉。管理好自己的演算法,適時進行剖析,但絕不要過早優化!

OneAPM 能夠幫你檢視 Python 應用程式的方方面面,不僅能夠監控終端的使用者體驗,還能監控伺服器效能,同時還支援追蹤資料庫、第三方 API 和 Web 伺服器的各種問題。想閱讀更多技術文章,請訪問 OneAPM 官方技術部落格

本文轉自 OneAPM 官方部落格

原文地址:http://blog.thehumangeo.com/2015/07/28/profiling-in-python/

相關文章