Python 效能分析入門指南

發表於2014-07-25

雖然並非你編寫的每個 Python 程式都要求一個嚴格的效能分析,但是讓人放心的是,當問題發生的時候,Python 生態圈有各種各樣的工具可以處理這類問題。

分析程式的效能可以歸結為回答四個基本問題:

  1. 正執行的多快
  2. 速度瓶頸在哪裡
  3. 記憶體使用率是多少
  4. 記憶體洩露在哪裡

下面,我們將用一些神奇的工具深入到這些問題的答案中去。

用 time 粗粒度的計算時間

讓我們開始通過使用一個快速和粗暴的方法計算我們的程式碼:傳統的 unix time 工具。

三個輸出測量值之間的詳細意義在這裡 stackoverflow article,但簡介在這:

  • real — 指的是實際耗時
  • user — 指的是核心之外的 CPU 耗時
  • sys — 指的是花費在核心特定函式的 CPU 耗時

你會有你的應用程式用完了多少 CPU 週期的即視感,不管系統上其他執行的程式新增的系統和使用者時間。

如果 sys 和 user 時間之和小於 real 時間,然後你可以猜測到大多數程式的效能問題最有可能與 IO wait 相關。

用 timing context 管理器細粒度的計算時間

我們下一步的技術包括直接嵌入程式碼來獲取細粒度的計時資訊。下面是我進行時間測量的程式碼的一個小片段

timer.py

為了使用它,使用 Python 的 with 關鍵字和 Timer 上下文管理器來包裝你想計算的程式碼。當您的程式碼塊開始執行,它將照顧啟動計時器,當你的程式碼塊結束的時候,它將停止計時器。

這個程式碼片段示例:

為了看看我的程式的效能隨著時間的演化的趨勢,我常常記錄這些定時器的輸出到一個檔案中。

使用 profiler 逐行計時和分析執行的頻率

羅伯特·克恩有一個不錯的專案稱為 line_profiler , 我經常使用它來分析我的指令碼有多快,以及每行程式碼執行的頻率:

為了使用它,你可以通過使用 pip 來安裝它:

安裝完成後,你將獲得一個新模組稱為 line_profiler 和 kernprof.py 可執行指令碼。

為了使用這個工具,首先在你想測量的函式上設定 @profile 修飾符。不用擔心,為了這個修飾符,你不需要引入任何東西。kernprof.py 指令碼會在執行時自動注入你的指令碼。

primes.py

一旦你得到了你的設定了修飾符 @profile 的程式碼,使用 kernprof.py 執行這個指令碼。

-l 選項告訴 kernprof 把修飾符 @profile 注入你的指令碼,-v 選項告訴 kernprof 一旦你的指令碼完成後,展示計時資訊。這是一個以上指令碼的類似輸出:

尋找 hits 值比較高的行或是一個高時間間隔。這些地方有最大的優化改進空間。

它使用了多少記憶體?

現在我們掌握了很好我們程式碼的計時資訊,讓我們繼續找出我們的程式使用了多少記憶體。我們真是非常幸運, Fabian Pedregosa 仿照 Robert Kern 的 line_profiler 實現了一個很好的記憶體分析器 [memory profiler][5]

首先通過 pip 安裝它:

在這裡建議安裝 psutil 是因為該包能提升 memory_profiler 的效能。

想 line_profiler 一樣, memory_profiler 要求在你設定 @profile 來修飾你的函式:

執行如下命令來顯示你的函式使用了多少記憶體:

一旦你的程式退出,你應該可以看到這樣的輸出:

line_profiler 和 memory_profiler 的 IPython 快捷命令

line_profiler 和 memory_profiler 一個鮮為人知的特性就是在 IPython 上都有快捷命令。你所能做的就是在 IPython 上鍵入以下命令:

這樣做了以後,你就可以使用魔法命令 %lprun 和 %mprun 了,它們表現的像它們命令列的副本,最主要的不同就是你不需要給你需要分析的函式設定 @profile 修飾符。直接在你的 IPython 會話上繼續分析吧。

這可以節省你大量的時間和精力,因為使用這些分析命令,你不需要修改你的原始碼。

哪裡記憶體溢位了?

cPython的直譯器使用引用計數來作為它跟蹤記憶體的主要方法。這意味著每個物件持有一個計數器,當增加某個物件的引用儲存的時候,計數器就會增加,當一個引用被刪除的時候,計數器就是減少。當計數器達到0, cPython 直譯器就知道該物件不再使用,因此直譯器將刪除這個物件,並且釋放該物件持有的記憶體。

記憶體洩漏往往發生在即使該物件不再使用的時候,你的程式還持有對該物件的引用。

最快速發現記憶體洩漏的方式就是使用一個由 Marius Gedminas 編寫的非常好的稱為 [objgraph][6] 的工具。
這個工具可以讓你看到在記憶體中物件的數量,也定位在程式碼中所有不同的地方,對這些物件的引用。

開始,我們首先安裝 objgraph

一旦你安裝了這個工具,在你的程式碼中插入一個呼叫偵錯程式的宣告。

哪個物件最常見

在執行時,你可以檢查在執行在你的程式中的前20名最普遍的物件

哪個物件被增加或是刪除了?

我們能在兩個時間點之間看到哪些物件被增加或是刪除了。

這個洩漏物件的引用是什麼?

繼續下去,我們還可以看到任何給定物件的引用在什麼地方。讓我們以下面這個簡單的程式舉個例子。

為了看到持有變數 X 的引用是什麼,執行 objgraph.show_backref() 函式:

該命令的輸出是一個 PNG 圖片,被儲存在 /tmp/backrefs.png,它應該看起來像這樣:

backrefs (1)

 

盒子底部有紅色字型就是我們感興趣的物件,我們可以看到它被符號 x 引用了一次,被列表 y 引用了三次。如果 x 這個物件引起了記憶體洩漏,我們可以使用這種方法來追蹤它的所有引用,以便看到為什麼它沒有被自動被收回。

回顧一遍,objgraph 允許我們:

  • 顯示佔用 Python 程式記憶體的前 N 個物件
  • 顯示在一段時期內哪些物件被增加了,哪些物件被刪除了
  • 顯示我們指令碼中獲得的所有引用

Effort vs precision

在這篇文章中,我展示瞭如何使用一些工具來分析一個python程式的效能。通過這些工具和技術的武裝,你應該可以獲取所有要求追蹤大多數記憶體洩漏以及在Python程式快速識別瓶頸的資訊。

和許多其他主題一樣,執行效能分析意味著要在付出和精度之間的平衡做取捨。當有疑問是,用最簡單的方案,滿足你當前的需求。

相關文章