深入理解 Python 虛擬機器:浮點數(float)的實現原理及原始碼剖析

一無是處的研究僧發表於2023-03-12

深入理解 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、計算機系統基礎、演算法與資料結構)知識。

相關文章