程式碼優化指南:人生苦短,我用Python

機器之心發表於2017-11-22

選自pythonfiles

機器之心編譯

參與:Panda

前段時間,Python Files 部落格釋出了幾篇主題為「Hunting Performance in Python Code」的系列文章,對提升 Python 程式碼的效能的方法進行了介紹。在其中的每一篇文章中,作者都會介紹幾種可用於 Python 程式碼的工具和分析器,以及它們可以如何幫助你更好地在前端(Python 指令碼)和/或後端(Python 直譯器)中找到瓶頸。機器之心對這個系列文章進行了整理編輯,將其融合成了這一篇深度長文。本文的相關程式碼都已經發布在 GitHub 上。

程式碼地址:https://github.com/apatrascu/hunting-python-performance


第一部分請檢視從環境設定到記憶體分析。以下是 Python 程式碼優化的第二部分,主要從 Python 指令碼與 Python 直譯器兩個方面闡述。在這一部分中我們首先會關注如何追蹤 Python 指令碼的 CPU 使用情況,並重點討論 cProfile、line_profiler、pprofile 和 vprof。而後一部分重點介紹了一些可用於在執行 Python 指令碼時對直譯器進行效能分析的工具和方法,主要討論了 CPython 和 PyPy 等。


CPU 分析——Python 指令碼

在這一節,我將介紹一些有助於我們解決 Python 中分析 CPU 使用的難題的工具。

CPU 效能分析(profiling)的意思是通過分析 CPU 執行程式碼的方式來分析這些程式碼的效能。也就是說要找到我們程式碼中的熱點(hot spot),然後看我們可以怎麼處理它們。

接下來我們會看看你可以如何追蹤你的 Python 指令碼的 CPU 使用。我們將關注以下分析器(profiler):

  • cProfile
  • line_profiler
  • pprofile
  • vprof


測量 CPU 使用

程式碼優化指南:人生苦短,我用Python
程式碼優化指南:人生苦短,我用Python

這一節我將使用與前一節基本一樣的指令碼,你也可以在 GitHub 上檢視:https://gist.github.com/apatrascu/8524679175de08a54a95e22001a31d3b

另外,記住在 PyPy2 上,你需要使用一個支援它的 pip 版本:

程式碼優化指南:人生苦短,我用Python

其它東西可以通過以下指令安裝:

程式碼優化指南:人生苦短,我用Python

cProfile

在 CPU 效能分析上最常用的一個工具是 cProfile,主要是因為它內建於 CPython2 和 PyPy2 中。這是一個確定性的分析器,也就是說它會在執行我們的負載時收集一系列統計資料,比如程式碼各個部分的執行次數或執行時間。此外,相比於其它內建的分析器(profile 或 hotshot),cProfile 對系統的開銷更少。

當使用 CPython2 時,其使用方法是相當簡單的:

程式碼優化指南:人生苦短,我用Python

如果你使用的是 PyPy2:

程式碼優化指南:人生苦短,我用Python

其輸出如下:

程式碼優化指南:人生苦短,我用Python

即使是這樣的文字輸出,我們也可以直接看到我們指令碼的大多數時間都在呼叫 list.append 方法。

如果我們使用 gprof2dot,我們可以用圖形化的方式來檢視 cProfile 的輸出。要使用這個工具,我們首先必須安裝 graphviz。在 Ubuntu 上,可以使用以下命令:

程式碼優化指南:人生苦短,我用Python

再次執行我們的指令碼:

程式碼優化指南:人生苦短,我用Python

然後我們會得到下面的 output.png 檔案:

程式碼優化指南:人生苦短,我用Python

這樣看起來就輕鬆多了。讓我們仔細看看它輸出了什麼。你可以看到來自指令碼的函式呼叫圖(callgraph)。在每個方框中,你可以一行一行地看到:

  • 第一行:Python 檔名、行數和方法名
  • 第二行:這個方框所用的時間佔全域性時間的比例
  • 第三行:括號中是該方法本身所用時間佔全域性時間的比例
  • 第四行:呼叫次數

比如說,在從上到下第三個紅色框中,方法 primes 佔用了 98.28% 的時間,65.44% 的時間是在該方法之中做什麼事情,它被呼叫了 40 次。剩下的時間被用在了 Python 的 list.append(22.33%)和 range(11.51%)方法中。

這是一個簡單的指令碼,所以我們只需要重寫我們的指令碼,讓它不用使用那麼多的 append 方法,結果如下:

程式碼優化指南:人生苦短,我用Python

以下測試了指令碼在使用前和使用 CPython2 後的執行時間:

程式碼優化指南:人生苦短,我用Python

用 PyPy2 測量:

程式碼優化指南:人生苦短,我用Python

我們在 CPython2 上得到了 2.4 倍的提升,在 PyPy2 上得到了 3.1 倍的提升。很不錯,其 cProfile 呼叫圖為:

程式碼優化指南:人生苦短,我用Python

你也可以以程式的方式檢視 cProfile:

程式碼優化指南:人生苦短,我用Python

這在一些場景中很有用,比如多程式效能測量。更多詳情請參閱:https://docs.python.org/2/library/profile.html#module-cProfile


line_profiler

這個分析器可以提供逐行水平的負載資訊。這是通過 C 語言用 Cython 實現的,與 cProfile 相比計算開銷更少。

其原始碼可在 GitHub 上獲取:https://github.com/rkern/line_profiler,PyPI 頁面為:https://pypi.python.org/pypi/line_profiler/。和 cProfile 相比,它有相當大的開銷,需要多 12 倍的時間才能得到一個分析結果。

要使用這個工具,你首先需要通過 pip 新增:pip install pip install Cython ipython==5.4.1 line_profiler(CPython2)。這個分析器的一個主要缺點是不支援 PyPy。

就像在使用 memory_profiler 時一樣,你需要在你想分析的函式上加上一個裝飾。在我們的例子中,你需要在 03.primes-v1.py 中的 primes 函式的定義前加上 @profile。然後像這樣呼叫:

程式碼優化指南:人生苦短,我用Python

你會得到一個這樣的輸出:

程式碼優化指南:人生苦短,我用Python

我們可以看到兩個迴圈在反覆呼叫 list.append,佔用了指令碼的大部分時間。


pprofile

地址:http://github.com/vpelletier/pprofile

據作者介紹,pprofile 是一個「行粒度的、可感知執行緒的確定性和統計性純 Python 分析器」。

它的靈感來源於 line_profiler,修復了大量缺陷,但因為其完全是用 Python 寫的,所以也可以通過 PyPy 使用。和 cProfile 相比,使用 CPython 時分析的時間會多 28 倍,使用 PyPy 時的分析時間會長 10 倍,但具有粒度更大的細節水平。

而且還支援 PyPy 了!除此之外,它還支援執行緒分析,這在很多情況下都很有用。

要使用這個工具,你首先需要通過 pip 安裝:pip install pprofile(CPython2)/ pypy -m pip install pprofile(PyPy),然後像這樣呼叫:

程式碼優化指南:人生苦短,我用Python

其輸出和前面工具的輸出不同,如下:

程式碼優化指南:人生苦短,我用Python
程式碼優化指南:人生苦短,我用Python

我們現在可以看到更詳細的細節。讓我們稍微研究一下這個輸出。這是這個指令碼的整個輸出,每一行你可以看到呼叫的次數、執行它所用的時間(秒)、每次呼叫的時間和佔全域性時間的比例。此外,pprofile 還為我們的輸出增加了額外的行(比如 44 和 50 行,行前面寫著 (call)),這是累積指標。

同樣,我們可以看到有兩個迴圈在反覆呼叫 list.append,佔用了指令碼的大部分時間。


vprof

地址:https://github.com/nvdv/vprof

vprof 是一個 Python 分析器,為各種 Python 程式特點提供了豐富的互動式視覺化,比如執行時間和記憶體使用。這是一個圖形化工具,基於 Node.JS,可在網頁上展示結果。

使用這個工具,你可以針對相關 Python 指令碼檢視下面的一項或多項內容:

  • CPU flame graph
  • 程式碼分析(code profiling)
  • 記憶體圖(memory graph)
  • 程式碼熱圖(code heatmap)

要使用這個工具,你首先需要通過 pip 安裝:pip install vprof(CPython2)/ pypy -m pip install vprof(PyPy),然後像這樣呼叫:

在 CPython2 上,要顯示程式碼熱圖(下面的第一行呼叫)和程式碼分析(下面的第二行呼叫):

程式碼優化指南:人生苦短,我用Python

在 PyPy 上,要顯示程式碼熱圖(下面的第一行呼叫)和程式碼分析(下面的第二行呼叫):

程式碼優化指南:人生苦短,我用Python

在上面的兩個例子中,你都會看到如下的程式碼熱圖:

程式碼優化指南:人生苦短,我用Python

以及如下的程式碼分析:

程式碼優化指南:人生苦短,我用Python

結果是以圖形化的方式展示的,你可以將滑鼠懸浮或點選每一行,從而檢視更多資訊。同樣,我們可以看到有兩個迴圈在反覆呼叫 list.append 方法,佔用了指令碼的大部分時間。


CPU 分析——Python 直譯器

在這一節,我將介紹一些可用於在執行 Python 指令碼時對直譯器進行效能分析的工具和方法。

正如前幾節提到的,CPU 效能分析的意義是一樣的,但現在我們的目標不是 Python 指令碼。我們現在想要知道 Python 直譯器的工作方式,以及 Python 指令碼執行時在哪裡消耗的時間最多。

接下來我們將看到你可以怎樣跟蹤 CPU 使用情況以及找到直譯器中的熱點。


測量 CPU 使用情況

這一節所使用的指令碼基本上和前面記憶體分析和指令碼 CPU 使用情況分析時使用的指令碼一樣,你也可以在這裡查閱程式碼:https://gist.github.com/apatrascu/44f0c6427e2df96951034b759e16946f

程式碼優化指南:人生苦短,我用Python

優化後的版本見下面或訪問:https://gist.github.com/apatrascu/ee660bf95469a55e5947a0066e930a69

程式碼優化指南:人生苦短,我用Python


CPython

CPython 的功能很多,這是完全用 C 語言寫的,因此在測量和/或效能分析上可以更加容易。你可以找到託管在 GitHub 上的 CPython 資源:https://github.com/python/cpython。預設情況下,你會看到最新的分支,在本文寫作時是 3.7+ 版本,但向前一直到 2.7 版本的分支都能找到。

在這篇文章中,我們的重點是 CPython 2,但最新的第 3 版也可成功應用同樣的步驟。


1. 程式碼覆蓋工具(Code coverage tool)

要檢視正在執行的 C 語言程式碼是哪一部分,最簡單的方法是使用程式碼覆蓋工具。

首先我們克隆這個程式碼庫:

程式碼優化指南:人生苦短,我用Python

複製該目錄中的指令碼並執行以下命令:

程式碼優化指南:人生苦短,我用Python

第一行程式碼將會使用 GCOV 支援(https://gcc.gnu.org/onlinedocs/gcc/Gcov.html)編譯該直譯器,第二行將執行負載並收集在 .gcda 檔案中的分析資料,第三行程式碼將解析包含這些分析資料的檔案並在名為 lcov-report 的資料夾中建立一些 HTML 檔案。

如果我們在瀏覽器中開啟 index.html,我們會看到為了執行我們的 Python 指令碼而執行的直譯器原始碼的位置。你會看到類似下面的東西:

程式碼優化指南:人生苦短,我用Python

在上面一層,我們可以看到構成該原始碼的每個目錄以及被覆蓋的程式碼的量。舉個例子,讓我們從 Objects 目錄開啟 listobject.c.gcov.html 檔案。儘管我們不會完全看完這些檔案,但我們會分析其中一部分。看下面這部分。

程式碼優化指南:人生苦短,我用Python

怎麼讀懂其中的資訊?在黃色一列,你可以看到 C 語言檔案程式碼的行數。接下來一列是特定一行程式碼執行的次數。最右邊一列是實際的 C 語言原始碼。

在這個例子中,listiter_next 方法被呼叫了 6000 萬次。

我們怎麼找到這個函式?如果我們仔細看看我們的 Python 指令碼,我們可以看到它使用了大量的列表迭代和 append。(這是另一個可以一開始就做指令碼優化的地方。)

讓我們繼續看看其它一些專用工具。在 Linux 系統上,如果我們想要更多資訊,我們可以使用 perf。官方文件可參閱:https://perf.wiki.kernel.org/index.php/Main_Page

我們使用下面的程式碼重建了 CPython 直譯器。你應該將這個 Python 指令碼下載到同一個目錄。另外,要確保你的系統安裝了 perf。

程式碼優化指南:人生苦短,我用Python

如下執行 perf。使用 perf 的更多方式可以看 Brendan Gregg 寫的這個:http://www.brendangregg.com/perf.html

程式碼優化指南:人生苦短,我用Python

執行指令碼後,你會看到下述內容:

程式碼優化指南:人生苦短,我用Python

要檢視結果,執行 sudo perf report 獲取指標。

程式碼優化指南:人生苦短,我用Python

只有最相關的呼叫會被保留。在上面的截圖中,我們可以看到佔用時間最多的是 PyEval_EvalFrameEx。這是其中的主直譯器迴圈,在這個例子中,我們對此並不關心。我們感興趣的是下一個耗時的函式 listiter_next,它佔用了 10.70% 的時間。

在執行了優化的版本之後,我們可以看到以下結果:

程式碼優化指南:人生苦短,我用Python

在我們優化之後,listiter_next 函式的時間佔用降至了 2.11%。讀者還可以探索對該直譯器進行進一步的優化。


2. Valgrind/Callgrind

另一個可用於尋找瓶頸的工具是 Valgrind,它有一個被稱為 callgrind 的外掛。更多細節請參閱:http://valgrind.org/docs/manual/cl-manual.html

我們使用下面的程式碼重建了 CPython 直譯器。你應該將這個 Python 指令碼下載到同一個目錄。另外,確保你的系統安裝了 valgrind。

程式碼優化指南:人生苦短,我用Python

按下面方法執行 valgrind:

程式碼優化指南:人生苦短,我用Python

結果如下:

程式碼優化指南:人生苦短,我用Python

我們使用 KCacheGrind 進行了視覺化:http://kcachegrind.sourceforge.net/html/Home.html

程式碼優化指南:人生苦短,我用Python

PyPy

在 PyPy 上,可以成功使用的分析器是非常有限的。PyPy 的開發者為此開發了工具 vmprof:https://vmprof.readthedocs.io/en/latest/

首先,你要下載 PyPy:https://pypy.org/download.html。在此之後,為其啟用 pip 支援。

程式碼優化指南:人生苦短,我用Python

安裝 vmprof 的方式很簡單,執行以下程式碼即可:

程式碼優化指南:人生苦短,我用Python

按以下方式執行工作負載:

程式碼優化指南:人生苦短,我用Python

然後在瀏覽器中開啟顯示在控制檯中的連結(以 http://vmprof.com/#/ 開頭的連結)。

程式碼優化指南:人生苦短,我用Python


原文連結:

https://pythonfiles.wordpress.com/2017/06/01/hunting-performance-in-python-code-part-3/
pythonfiles.wordpress.com/2017/08/24/…


相關文章