Python 中的 Integer Cache 介紹

Feature image

Source: Unsplash

前言

本篇文章想和大家分享 Python Language 中的一個 Trick — Small Integer Cache,這個 Trick 的目的是為了提昇 Python 程式的運作效率,而且已經被實做在 Python 中。

本篇文章適合對於 Python 程式語言已經有基本認識,且希望學習一些 Python 的底層知識的讀者!如果你還是一名程式初學者,並且想從 Python 開始學習的話,可以參考 Python 教學系列文章

Small Integer Cache

為了體現 Python 中 Small Integer Cache 的概念,我們可以先看看以下程式碼執行的結果:

Python 3.12.5 | packaged by Anaconda, Inc. | (main, Sep 12 2024, 18:27:27) [GCC 11.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> a = 100
>>> b = 100
>>> print(id(a) == id(b))
True
>>> c = 257
>>> d = 257
>>> print(id(c) == id(d))
False
>>>

在 Python 中,我們可以透過 id() 函式來取得一個物件 (Object) 的 Memory Address。因此,從上面的執行結果可以發現 a 和 b 明明是不同的兩個變數,但是他們的 Memory Address 卻一樣,而 c 和 d 的 Memory Address 又相同?!

會發生上面的狀況主要是 Python 語言的實做中包含了 Integer Cache 的機制:

Integer Cache from Python Document

Integer Cache from Python Document

AD

如上圖所示,從 Python 的官方文件中我們可以發現:對於一些比較常見的 Integer [-5, 256],Python 會事先建立 (Pre-Allocated) 這些 Integer 的 Object,當我們在程式碼中宣告的 Integer 在這個範圍時,其實都會 Reference 到同一個 Object 而不會建立新的 Object。

這也就清楚的說明了,為什麼在上面的程式執行結果上,變數 a 和 b 會指到同一個 Memory Address 而變數 c 和 d 則是指到不同的 Memory Address。這個 Integer Cache 的目的當然是為了讓 Python 程式的運作更有效率,針對比較常出現在程式中的 Integer,減少做 Memory Allocation 的次數。

Reference Count

為了說明 [-5, 256] 這個區間的 Integer 確實比較常被使用到,我們也可以印出這些 Integer Object 的 Reference Count。所謂的 Reference Count 是指一個 Object 被 Reference 到的次數。

更具體的來說,以 CPython 所實做的 Python 版本為例,Integer 在 CPython 中的表示會是

struct _longobject {
    PyObject_VAR_HEAD
    digit ob_digit[1];
};

其中,從 Python 官方文件我們可以看到 PyObject_VAR_HEAD 這個 Macro 其實是表示:

PyVarObject ob_base;

再回到 CPython 的原始碼中,我們可以發現 PyVarObject 其實就是把 PyObject 再加上一個 ob_size 的欄位

typedef struct _object {
    _PyObject_HEAD_EXTRA
    Py_ssize_t ob_refcnt;
    struct _typeobject *ob_type;
} PyObject;

typedef struct {
    PyObject ob_base;
    Py_ssize_t ob_size; /* Number of items in variable part */
} PyVarObject;

因此,最終一個 Integer Object 在 Python 中其實會是:

struct _longobject {
    long ob_refcnt;
    PyTypeObject *ob_type;
    size_t ob_size;
    long ob_digit[1];
};

ob_refcnt 就是表示這個 Integer Object 的 Reference Count!

在 Python 中我們可以透過 sys.getrefcount() 函式來取得一個 Object 的 Reference Count,並透過一些繪圖套件 (ex. matplotlib) 呈現這些 Integer Object 被 Reference 到的次數分佈:

Reference counts of interger values

Reference counts of interger values (source: https://www.codementor.io)

可以發現到在還沒有加入自己的其他 Code  之前,光是 Python Interpreter/Compiler 內部的運作就已經對這些 Smaller Integer 做了很多次 Reference。透過 Integer Cache 的機制確實可以減少對這些經常用到的 Integer Object 做 Memory Allocation。

AD

Python REPL vs Python File

在上述的程式碼範例中,是在 Python REPL 或是 Python Shell 環境中進行的。如果我們先將程式碼打在一個 Python File (ex. main.py) 中,再透過 python main.py 的方式執行,會發現不管針對什麼範圍的 Integer,只要是相同的 Integer,兩個變數的 Memory Address 就會是一樣的。這是因為當我們透過 python main.py 執行程式碼時,Python 中的 Compiler 其實已經可以先針對所有的 Code 做分析並且優化,當它發現到我們有兩個 Integer Object 的數值其實是一樣的時候,就會讓它們 Reference 到相同的 Object,來減少 Memory 的浪費。

而在 Python REPL 環境中,因為 Python 的 Compiler 每一次都只能讀取一行 Code,因此在優化上就會有所限制,而將 Integer Cache 的機制赤裸裸的呈現。

結語

本篇文章中我們介紹了Python 中的 Integer Cache 機制是如何作用在 [-5, 256] 範圍的 Integer 上,同時透過 CPython 的原始碼來說明這些 Integer Object 的 Reference Count。

參考資料