用 Snakeviz 找出 Python 程式碼速度瓶頸

Python 比起 C 語言等低階程式碼,雖然執行速度絕對慢上數以千倍,但在這個 CPU 效能早已過剩——動輒數 GHz 指令執行頻率——的時代。除非真有龐大資料1需要處理,否則日常使用上,Python 的「慢」還真感受不出來。

但,人生就是這個但是。

我寫的程式,就是慢啊~不知道為什麼,只是跑個簡單的迴圈竟然就要花上 1 分鐘!?

這時候推薦使用 Snakeviz,我們可以用它來找出拖慢 Python 執行效率的罪魁禍首。也就是要來做 Profiling!

範例程式

本文用底下範例程式來說明。這是一支不使用遞迴的斐波那契函式。

def fibonacci(n):
    stack = [n]
    result = 0
    while stack:
        current = stack.pop(0)
        if current <= 0:
            result += 0
        elif current == 1:
            result += 1
        else:
            stack.append(current - 1)
            stack.append(current - 2)
    return result

def main():
    fibonacci(27)

if __name__ == "__main__":
    main()

看起來很簡單,但在我的 M1 MacBook Air 上大約需要跑 15 秒。超慢!

來分析下。

Snakeviz

安裝

$ pip install snakeviz

Profiling

使用步驟:

1. 先用 Python 內建模組 cProfile 側錄一下我們的程式2

$ python -m cProfile -o program.prof fab.py

2. 再用 Snakeviz3來查看側錄結果。

$ snakeviz program.prof

看結果

畫面主要分成 2 區:左邊是顯示設定,中間的色塊則是 Function Call 時間統計。也就是我們在意的重點!範例程式的分析圖長這樣:

排列方式是由上往下呼叫。也就是:

  1. Python 執行 Script 的內建呼叫 exec
  2. 執行 fab.py 第 1 行(Python 模組開始)
  3. 進入 fab.py 第 15 行(跨號內顯示 Function 名稱,此時是 main 函式)
  4. 進入 fab.py 第 1 行(fibonacci 函式呼叫)
  5. ⋯⋯下面依此類推

每個 Function Call 都會統計執行時間,紀錄於各色塊下方。由圖可發現, 15 秒內大部分的時間都花在執行 List 中的 pop 方法!

解決效能瓶頸

知道問題出在哪,就可以來解決了。

Google 一番後,發現問題出在 pop(0) 這操作。每次都需要重新將 List 後面的所有元素往前移動一格,因此相當浪費資源。以範例來說,其實只需要 2 個需求:

  • 拿取第一個元素
  • 在最後面增加元素

而最適合這 2 種操作的資料結構是 Deque!改寫一下。

from collections import deque

def fibonacci(n):
    stack = deque([n])
    result = 0
    while stack:
        current = stack.popleft()
        if current <= 0:
            result += 0
        elif current == 1:
            result += 1
        else:
            stack.append(current - 1)
            stack.append(current - 2)
    return result

def main():
    fibonacci(27)

if __name__ == "__main__":
    main()

再跑一次 Profiling。Boom!

這次,不用 1 秒就結束了。搞定!

效能最佳化

真實情況下,不可能每次都像本文範例般如此明顯。更多時候,我們需要自行截取出速度慢的程式片段,單獨寫成一支小 Script 來做 Profiling。並且,通常會長成如下圖般,乍看之下沒有特別花時間的地方。

圖中的色塊每個都可以點開。再繼續往下追,看看有沒有不預期花太多時間處理的函式。一個一個點開來檢視,看看有無辦法加速。一些常見的做法是:

  • 使用 functools.cache。為經常呼叫的 Function 快取返回值。
  • 使用 pickle.dumps 保存肥大 Python 物件,在其他地方直接使用 loads 處理。
  • 使用 Threading 或者 Multiprocessing 同時處理。
  • 盡可能減少檔案讀取或網路存取次數
  • ⋯⋯等等

我想說的是,比起 I/O(硬碟寫入或讀取)、網路爬蟲等常用操作,Python 絕對不慢!雖然會使用 Python 的場景通常都不要求效能。但為了生活我可以忍,可是浪費我們寶貴時間就不行!

 

 

Related Tools

Footnotes

  1. 真的需要處理大數據的 Python 模組,最終還是會使用 C 語言,以做出效能更高的程式庫來呼叫。
  2. 側錄的時候,會跑的比原本還慢。因為側錄程式會需要多紀錄每個函式呼叫的使用情況。
  3. 其實 Snakeviz 只是提供一個用漂亮介面,來顯示 cProfile 側錄出來的結果。