深入理解 Python 虛擬機器:整型(int)的實現原理及原始碼剖析
在本篇文章當中主要給大家介紹在 cpython 內部是如何實現整型資料 int 的,主要是分析 int 型別的表示方式,分析 int 型別的巧妙設計。
資料結構
在 cpython 內部的 int 型別的實現資料結構如下所示:
typedef struct _longobject PyLongObject;
struct _longobject {
PyObject_VAR_HEAD
digit ob_digit[1];
};
#define PyObject_VAR_HEAD PyVarObject ob_base;
typedef struct {
PyObject ob_base;
Py_ssize_t ob_size; /* Number of items in variable part */
} PyVarObject;
typedef struct _object {
_PyObject_HEAD_EXTRA
Py_ssize_t ob_refcnt;
struct _typeobject *ob_type;
} PyObject;
上面的資料結構用圖的方式表示出來如下圖所示:
- ob_refcnt,表示物件的引用記數的個數,這個對於垃圾回收很有用處,後面我們分析虛擬機器中垃圾回收部分在深入分析。
- ob_type,表示這個物件的資料型別是什麼,在 python 當中有時候需要對資料的資料型別進行判斷比如 isinstance, type 這兩個關鍵字就會使用到這個欄位。
- ob_size,這個欄位表示這個整型物件陣列 ob_digit 當中一共有多少個元素。
- digit 型別其實就是 uint32_t 型別的一個 宏定義,表示 32 位的整型資料。
深入分析 PyLongObject 欄位的語意
首先我們知道在 python 當中的整數是不會溢位的,這正是 PyLongObject 使用陣列的原因。在 cpython 內部的實現當中,整數有 0 、正數、負數,對於這一點在 cpython 當中有以下幾個規定:
- ob_size,儲存的是陣列的長度,ob_size 大於 0 時儲存的是正數,當 ob_size 小於 0 時儲存的是負數。
- ob_digit,儲存的是整數的絕對值。在前面我們談到了,ob_digit 是一個 32 位的資料,但是在 cpython 內部只會使用其中的前 30 位,這隻為了避免溢位的問題。
我們下面使用幾個例子來深入理解一下上面的規則:
在上圖當中 ob_size 大於 0 ,說明這個數是一個正數,而 ob_digit 指向一個 int32 的資料,數的值等於 10,因此上面這個數表示整數 10 。
同理 ob_size 小於 0,而 ob_digit 等於 10,因此上圖當中的資料表示 -10 。
上面是一個 ob_digit 陣列長度為 2 的例子,上面所表示資料如下所示:
因為對於每一個陣列元素來說我們只使用前 30 位,因此到第二個整型資料的時候正好對應著 \(2^{30}\),大家可以對應著上面的結果瞭解整個計算過程。
上面也就很簡單了:
小整數池
為了避免頻繁的建立一些常用的整數,加快程式執行的速度,我們可以將一些常用的整數先快取起來,如果需要的話就直接將這個資料返回即可。在 cpython 當中相關的程式碼如下所示:(小整數池當中快取資料的區間為[-5, 256])
#define NSMALLPOSINTS 257
#define NSMALLNEGINTS 5
static PyLongObject small_ints[NSMALLNEGINTS + NSMALLPOSINTS];
我們使用下面的程式碼進行測試,看是否使用了小整數池當中的資料,如果使用的話,對於使用小整數池當中的資料,他們的 id() 返回值是一樣的,id 這個內嵌函式返回的是 python 物件的記憶體地址。
>>> a = 1
>>> b = 2
>>> c = 1
>>> id(a), id(c)
(4343136496, 4343136496)
>>> a = -6
>>> c = -6
>>> id(a), id(c)
(4346020624, 4346021072)
>>> a = 257
>>> b = 257
>>> id(a), id(c)
(4346021104, 4346021072)
>>>
從上面的結果我們可以看到的是,對於區間[-5, 256]當中的值,id 的返回值確實是一樣的,不在這個區間之內的返回值就是不一樣的。
我們還可以這個特性實現一個小的 trick,就是求一個 PyLongObject 物件所佔的記憶體空間大小,因為我們可以使用 -5 和 256 這兩個資料的記憶體首地址,然後將這個地址相減就可以得到 261 個 PyLongObject 所佔的記憶體空間大小(注意雖然小整數池當中一共有 262 個資料,但是最後一個資料是記憶體首地址,並不是尾地址,因此只有 261 個資料),這樣我們就可以求一個 PyLongObject 物件的記憶體大小。
>>> a = -5
>>> b = 256
>>> (id(b) - id(a)) / 261
32.0
>>>
從上面的輸出結果我們可以看到一個 PyLongObject 物件佔 32 個位元組。我們可以使用下面的 C 程式檢視一個 PyLongObject 真實所佔的記憶體空間大小。
#include "Python.h"
#include <stdio.h>
int main()
{
printf("%ld\n", sizeof(PyLongObject));
return 0;
}
上面的程式的輸出結果如下所示:
上面兩個結果是相等的,因此也驗證了我們的想法。
從小整數池當中獲取資料的核心程式碼如下所示:
static PyObject *
get_small_int(sdigit ival)
{
PyObject *v;
assert(-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS);
v = (PyObject *)&small_ints[ival + NSMALLNEGINTS];
Py_INCREF(v);
return v;
}
整數的加法實現
關於 PyLongObject 的操作有很多,我們看一下加法的實現,見微知著,剩下的其他的方法我們就不介紹了,大家感興趣可以去看具體的原始碼。
如果你瞭解過大整數加法就能夠知道,大整數加法的具體實現過程了,在 cpython 內部的實現方式其實也是一樣的,就是不斷的進行加法操作然後進行進位操作。
#define Py_ABS(x) ((x) < 0 ? -(x) : (x)) // 返回 x 的絕對值
#define PyLong_BASE ((digit)1 << PyLong_SHIFT)
#define PyLong_MASK ((digit)(PyLong_BASE - 1))
static PyLongObject *
x_add(PyLongObject *a, PyLongObject *b)
{
// 首先獲得兩個整型資料的 size
Py_ssize_t size_a = Py_ABS(Py_SIZE(a)), size_b = Py_ABS(Py_SIZE(b));
PyLongObject *z;
Py_ssize_t i;
digit carry = 0;
// 確保 a 儲存的資料 size 是更大的
/* Ensure a is the larger of the two: */
if (size_a < size_b) {
{ PyLongObject *temp = a; a = b; b = temp; }
{ Py_ssize_t size_temp = size_a;
size_a = size_b;
size_b = size_temp; }
}
// 建立一個新的 PyLongObject 物件,而且陣列的長度是 size_a + 1
z = _PyLong_New(size_a+1);
if (z == NULL)
return NULL;
// 下面就是整個加法操作的核心
for (i = 0; i < size_b; ++i) {
carry += a->ob_digit[i] + b->ob_digit[i];
// 將低 30 位的資料儲存下來
z->ob_digit[i] = carry & PyLong_MASK;
// 將 carry 右移 30 位,如果上面的加法有進位的話 剛好可以在下一次加法當中使用(注意上面的 carry)
// 使用的是 += 而不是 =
carry >>= PyLong_SHIFT; // PyLong_SHIFT = 30
}
// 將剩下的長度儲存 (因為 a 的 size 是比 b 大的)
for (; i < size_a; ++i) {
carry += a->ob_digit[i];
z->ob_digit[i] = carry & PyLong_MASK;
carry >>= PyLong_SHIFT;
}
// 最後儲存高位的進位
z->ob_digit[i] = carry;
return long_normalize(z); // long_normalize 這個函式的主要功能是保證 ob_size 儲存的是真正的資料的長度 因為可以是一個正數加上一個負數 size 還變小了
}
PyLongObject *
_PyLong_New(Py_ssize_t size)
{
PyLongObject *result;
/* Number of bytes needed is: offsetof(PyLongObject, ob_digit) +
sizeof(digit)*size. Previous incarnations of this code used
sizeof(PyVarObject) instead of the offsetof, but this risks being
incorrect in the presence of padding between the PyVarObject header
and the digits. */
if (size > (Py_ssize_t)MAX_LONG_DIGITS) {
PyErr_SetString(PyExc_OverflowError,
"too many digits in integer");
return NULL;
}
// offsetof 會呼叫 gcc 的一個內嵌函式 __builtin_offsetof
// offsetof(PyLongObject, ob_digit) 這個功能是得到 PyLongObject 物件 欄位 ob_digit 之前的所有欄位所佔的記憶體空間的大小
result = PyObject_MALLOC(offsetof(PyLongObject, ob_digit) +
size*sizeof(digit));
if (!result) {
PyErr_NoMemory();
return NULL;
}
// 將物件的 result 的引用計數設定成 1
return (PyLongObject*)PyObject_INIT_VAR(result, &PyLong_Type, size);
}
static PyLongObject *
long_normalize(PyLongObject *v)
{
Py_ssize_t j = Py_ABS(Py_SIZE(v));
Py_ssize_t i = j;
while (i > 0 && v->ob_digit[i-1] == 0)
--i;
if (i != j)
Py_SIZE(v) = (Py_SIZE(v) < 0) ? -(i) : i;
return v;
}
總結
在本篇文章當中主要給大家介紹了 cpython 內部是如何實現整型資料 int 的,分析了 int 型別的表示方式和設計。int 內部使用 digit 來表示 32 位的整型資料,同時為了避免溢位的問題,只會使用其中的前 30 位。在 cpython 內部的實現當中,整數有 0 、正數、負數,對於這一點有以下幾個規定:
- ob_size,儲存的是陣列的長度,ob_size 大於 0 時儲存的是正數,當 ob_size 小於 0 時儲存的是負數。
- ob_digit,儲存的是整數的絕對值。
- 此外,為避免頻繁建立一些常用的整數,cpython 使用了小整數池的技術,將一些常用的整數先快取起來。最後,本文還介紹了整數的加法實現,即不斷進行加法操作然後進行進位操作。
cpython 使用這種方式的主要原理就是大整數的加減乘除,本篇文章主要是介紹了加法操作,大家如果感興趣可以自行閱讀其他的源程式。
本篇文章是深入理解 python 虛擬機器系列文章之一,文章地址:https://github.com/Chang-LeHung/dive-into-cpython
更多精彩內容合集可訪問專案:https://github.com/Chang-LeHung/CSCore
關注公眾號:一無是處的研究僧,瞭解更多計算機(Java、Python、計算機系統基礎、演算法與資料結構)知識。