Python 先天的設計使然,所以 Python 相較起其他語言(如 Rust, Go 等)更加耗用記憶體,如果是簡單的 Python 程式(例如自動化腳本)基本不需要太在意記憶體的問題,但如果是需要計較運算資源成本的應用場景,例如使用 AWS, GCP 雲端服務等, Python 的記憶體耗用肯定會對總體花費造成影響,因此了解你的應用使用記憶體的情形是相當重要的一步。

如果要優化記憶體的使用,可先從盤點尖峰記憶體的用量著手,除了可以避免使用等級過高的運算資源之外,還可以針對尖峰用量最大的應用優先降低用量。

Python 也提供內建的 resource 模組可使用,讓開發者可以設定系統資源的使用限制(例如 CPU time, heap size, nice 值 等等),也可以讀取系統資源使用的情況,其中就包含尖峰記憶體(peak memory)用量的資訊。

本文環境

  • macOS 11.6
  • Python 3.9

resource.getusage()

如要取得 Python 程式執行時使用系統資源的情況,可以呼叫 resource 模組所提供的 getusage() 方法

以下是使用 resource.getusage() 取得尖峰記憶體用量的範例,從該範例可以看到我們故意創造 1 個相當大的 list, 並且在最後以 getrusage(RUSAGE_SELF).ru_maxrss 取得尖峰記憶體用量的資訊:

from resource import getrusage, RUSAGE_SELF


huge_list = [x for x in range(0, 10_000_000)]

print("peak memory:", getrusage(RUSAGE_SELF).ru_maxrss / 1000 / 1000, "MB")

上述範例執行結果如下,可以看到光是將 1 千萬個整數存進 list 就可能耗用 400 多 MB 的記憶體:

$ python test.py
peak memory: 412.29926400000005 MB

實際上 getusage() 會因為所傳入的參數不同,而有不同的計算方式,本文範例中的 RUSAGE_SELF 是計算包含所有執行緒在內的系統資源用量。

Pass to getrusage() to request resources consumed by the calling process, which is the sum of resources used by all threads in the process.

關於不同的參數說明,可以進一步閱讀 Python 官方文件

getusage() 回傳值中含有多個資訊,例如 CPU time, 記憶體相關資訊等(詳見文件),其中 ru_maxrss 就是尖峰記憶體用量,不過 ru_maxrss 的單位會因為作業系統(OS)有所不同,例如 macOS 是以 bytes 為單位,而 Linux 是 kilobytes, 因此使用上需要特別注意單位的問題。

而前述範例,可以搭配 atexit 模組進一步改寫為以下形式,如此一來在 Python 程式結束執行時,就會自動將尖峰記憶體用量列印出來:

import sys
import atexit

from resource import getrusage, RUSAGE_SELF


@atexit.register
def print_peak_memory():
    divisor = 1000
    if sys.platform.startswith('darwin'):
        divisor = 1000 * 1000
    print("peak memory:", getrusage(RUSAGE_SELF).ru_maxrss / divisor, "MB")


huge_list = [x for x in range(0, 10_000_000)]

同場加映

了解尖峰記憶體用量之後,就能夠進一步開始優化記憶體的用量,前述範例可以看到光是存 1 千萬個整數到 list 之中就會耗費 400 多 MB 的記憶體,如果要改善該情況,可以利用 NumPy 所提供的高效能的陣列相關函式(例如 arange )儲存資料,將可以節省大量記憶體:

import sys
import numpy as np
import atexit

from resource import getrusage, RUSAGE_SELF


@atexit.register
def print_peak_memory():
    divisor = 1000
    if sys.platform.startswith('darwin'):
        divisor = 1000 * 1000
    print("peak memory:", getrusage(RUSAGE_SELF).ru_maxrss / divisor, "MB")


huge_list = np.arange(10_000_000)

上述範例執行結果如下,可以發現改用 NumPy 儲存 1 千萬個整數時,尖峰記憶體用量被減少至 100 MB 左右,足足減少 300 MB 左右,數量相當十分可觀:

$ python test.py
peak memory: 104.513536 MB

以上!

Happy Coding!

References

https://docs.python.org/3/library/resource.html