深入理解 Python 虛擬機器:浮點數(float)的實現原理及原始碼剖析
在本篇文章當中主要分析在 cpython 虛擬機器當中 float 型別的實現原理以及與他相關的一些原始碼。
Float 資料結構
在 cpython 虛擬機器當中浮點數型別的資料結構定義如下所示:
typedef struct {
PyObject_HEAD
double ob_fval;
} PyFloatObject;
上面的資料結構定義圖示如下:
- 在上面的資料結構當中最重要的一個欄位就是 ob_fval,這個就是真實儲存浮點數的地方。
- ob_refcnt 就是物件的引用計數。
- ob_type 就是物件的型別。
浮點數的相關方法
建立 float 物件
和我們在前面所討論到的元組和列表物件一樣,在 cpython 內部實現 float 型別的時候也會給 float 物件做一層中間層以加快浮點數的記憶體分配,具體的相關程式碼如下所示:
#define PyFloat_MAXFREELIST 100
static int numfree = 0;
static PyFloatObject *free_list = NULL;
在 cpython 內部做多會快取 100 個 float 物件的記憶體空間,如果超過 100 就會直接釋放記憶體了,這裡需要注意一點的是隻用一個指標就可以將所有的 float 物件快取起來,這一點是如何實現的。
這是使用在物件 PyFloatObject 當中的 struct _typeobject *ob_type; 這個欄位實現的,用這個欄位指向下一個 float 物件的記憶體空間,因為在 free_list 當中的資料並沒有使用,因此可以利用這個特點節省一些記憶體空間。下面則是建立 float 物件的具體過程:
PyObject *
PyFloat_FromDouble(double fval)
{
// 首先檢視 free_list 當中是否有空閒的 float 物件
PyFloatObject *op = free_list;
if (op != NULL) {
// 如果有 那麼就將讓 free_list 指向 free_list 當中的下一個 float 物件 並且將對應的個數減 1
free_list = (PyFloatObject *) Py_TYPE(op);
numfree--;
} else {
// 否則的話就需要申請記憶體空間
op = (PyFloatObject*) PyObject_MALLOC(sizeof(PyFloatObject));
if (!op)
return PyErr_NoMemory();
}
/* Inline PyObject_New */
(void)PyObject_INIT(op, &PyFloat_Type); // PyObject_INIT 這個宏的主要作用是將物件的引用計數設定成 1
op->ob_fval = fval;
return (PyObject *) op;
}
加法
下面是在 cpython 當中浮點數的加法具體實現,整個過程比較簡單就是得到新的值,並且建立一個新的 PyFloatObject 物件,並且將這個物件返回。
static PyObject *
float_add(PyObject *v, PyObject *w)
{
double a,b;
CONVERT_TO_DOUBLE(v, a); // CONVERT_TO_DOUBLE 這個宏的主要作用就是將物件的 ob_fval 這個欄位的值儲存到 a 當中
CONVERT_TO_DOUBLE(w, b); // 這個就是將 w 當中的 ob_fval 欄位的值儲存到 b 當中
a = a + b;
return PyFloat_FromDouble(a); // 建立一個新的 float 物件 並且將這個物件返回
}
減法
同理減法也是一樣的。
static PyObject *
float_sub(PyObject *v, PyObject *w)
{
double a,b;
CONVERT_TO_DOUBLE(v, a);
CONVERT_TO_DOUBLE(w, b);
a = a - b;
return PyFloat_FromDouble(a);
}
乘法
static PyObject *
float_mul(PyObject *v, PyObject *w)
{
double a,b;
CONVERT_TO_DOUBLE(v, a);
CONVERT_TO_DOUBLE(w, b);
PyFPE_START_PROTECT("multiply", return 0)
a = a * b;
PyFPE_END_PROTECT(a)
return PyFloat_FromDouble(a);
}
除法
static PyObject *
float_div(PyObject *v, PyObject *w)
{
double a,b;
CONVERT_TO_DOUBLE(v, a);
CONVERT_TO_DOUBLE(w, b);
if (b == 0.0) {
PyErr_SetString(PyExc_ZeroDivisionError,
"float division by zero");
return NULL;
}
a = a / b;
return PyFloat_FromDouble(a);
}
取反
這裡加入了一行輸出語句,這個是為了後面方便我們進行測試的。
static PyObject *
float_neg(PyFloatObject *v)
{
printf("%.2lf 正在進行取反運算\n", v->ob_fval);
return PyFloat_FromDouble(-v->ob_fval);
}
求絕對值
static PyObject *
float_abs(PyFloatObject *v)
{
printf("%.2lf 正在進行取 abs 運算\n", v->ob_fval);
return PyFloat_FromDouble(fabs(v->ob_fval));
}
求 bool 值
static int
float_bool(PyFloatObject *v)
{
printf("%.2lf 正在進行取 bool 運算\n", v->ob_fval);
return v->ob_fval != 0.0;
}
下圖是我們對於 cpython 對程式的修改!
下面是修改之後我們再次對浮點數進行操作的時候的輸出,可以看到的是輸出了我們在上面的程式碼當中加入的語句。
總結
在本篇文章當總主要介紹了一些 float 型別在 cpython 內部是如何實現的以及和他相關的加減乘除方法是如何實現的,以及和部分和關鍵字有關的函式實現。本篇文章主要是討論 float 資料型別本身,不涉及其他的東西,其實關於型別還有非常大一塊,就是 cpython 內部物件系統是如何實現的,這一點在後面深入討論物件系統的時候再進行深入分析,在回頭來看 float 型別會有更加深刻的理解。
本篇文章是深入理解 python 虛擬機器系列文章之一,文章地址:https://github.com/Chang-LeHung/dive-into-cpython
更多精彩內容合集可訪問專案:https://github.com/Chang-LeHung/CSCore
關注公眾號:一無是處的研究僧,瞭解更多計算機(Java、Python、計算機系統基礎、演算法與資料結構)知識。