2.1 Python3 float詳解
float內部結構
首先在檔案Include/floatobject.h中,找到了float例項物件的結構體:
typedef struct {
PyObject_HEAD
double ob_fval;
} PyFloatObject;
除了定長物件的共用頭部,只有一個欄位ob_fval,這個欄位就是用來儲存浮點物件的浮點值的。
在回顧一下float型別物件的結構體。float型別物件是系統內建的型別物件,是全域性唯一的,因此可以作為全域性變數定義。在檔案Objects/floatobject.c中:
PyTypeObject PyFloat_Type = { PyVarObject_HEAD_INIT(&PyType_Type, 0) "float", sizeof(PyFloatObject), 0, (destructor)float_dealloc, /* tp_dealloc */ 0, /* tp_vectorcall_offset */ 0, /* tp_getattr */ 0, /* tp_setattr */ 0, /* tp_as_async */ (reprfunc)float_repr, /* tp_repr */ &float_as_number, /* tp_as_number */ 0, /* tp_as_sequence */ 0, /* tp_as_mapping */ (hashfunc)float_hash, /* tp_hash */ 0, /* tp_call */ 0, /* tp_str */ PyObject_GenericGetAttr, /* tp_getattro */ 0, /* tp_setattro */ 0, /* tp_as_buffer */ Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | _Py_TPFLAGS_MATCH_SELF, /* tp_flags */ float_new__doc__, /* tp_doc */ 0, /* tp_traverse */ 0, /* tp_clear */ float_richcompare, /* tp_richcompare */ 0, /* tp_weaklistoffset */ 0, /* tp_iter */ 0, /* tp_iternext */ float_methods, /* tp_methods */ 0, /* tp_members */ float_getset, /* tp_getset */ 0, /* tp_base */ 0, /* tp_dict */ 0, /* tp_descr_get */ 0, /* tp_descr_set */ 0, /* tp_dictoffset */ 0, /* tp_init */ 0, /* tp_alloc */ float_new, /* tp_new */ .tp_vectorcall = (vectorcallfunc)float_vectorcall, };
PyFloat_Type儲存了float物件的元資訊,這些元資訊決定了浮點例項物件的生死和行為,關鍵欄位如下:
- tp_name:型別的名稱,這裡是常量'float';
- tp_dealloc、tp_init、tp_alloc、tp_new:物件建立和銷燬的相關函式;
- tp_repr:生成語法字串表示形式的函式;
- tp_str:生成普通字串表示形式的函式;
- tp_as_number:數值操作集;
- tp_hash:雜湊值生成函式;
float例項的建立
float例項物件的建立流程前面的章節已經介紹過了,再來回顧一下使用通用流程建立物件的過程:Python執行的是type型別物件當中的tp_call函式。tp_call函式進而呼叫float型別物件的tp_new和tp_init函式建立例項物件並進行初始化。
在原始碼中,PyFloat_Type的tp_init函式指標為空,這是因為float是一種很簡單的物件,初始化操作就是一個賦值語句,在tp_new中完成即可。
除了通用流程,Python為內建物件實現了物件建立API,簡化呼叫,提高效率。比如直接建立浮點物件:
>>> pi = 3.14
這裡其實是通過PyFloat_FromDouble函式實現的,直接將浮點值建立成浮點物件:
PyObject * PyFloat_FromDouble(double fval) { PyFloatObject *op = free_list; if (op != NULL) { 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); op->ob_fval = fval; return (PyObject *) op; }
- 首先為物件分配記憶體空間(PyObject_MALLOC函式),優先使用空閒物件快取池。
- 初始化物件型別欄位ob_type以及引用計數字段ob_refcnt(PyObject_INIT);
- 將ob_fval欄位初始化為浮點值。
float例項的銷燬
當物件的某次引用被解除時,Python通過Py_DECREF或者Py_XDECREF巨集減少引用計數;當引用計數降為0時,Python通過_Py_Dealloc巨集回收物件。
_Py_Dealloc巨集呼叫型別物件PyFloat_Type中的tp_dealloc函式指標:
#define _Py_Dealloc(op) ( \
_Py_INC_TPFREES(op) _Py_COUNT_ALLOCS_COMMA \
(*Py_TYPE(op)->tp_dealloc)((PyObject *)(op)))
根據程式碼可知,float回收物件實際呼叫的函式是float_dealloc:
static void
float_dealloc(PyFloatObject *op)
{
if (PyFloat_CheckExact(op)) {
if (numfree >= PyFloat_MAXFREELIST) {
PyObject_FREE(op);
return;
}
numfree++;
Py_TYPE(op) = (struct _typeobject *)free_list;
free_list = op;
}
else
Py_TYPE(op)->tp_free((PyObject *)op);
}
總結一下,float例項物件從建立到銷燬整個生命週期所涉及的關鍵函式、巨集以及呼叫關係如下:
空閒物件快取池
浮點運算是比較常見的運算方式之一。其實浮點運算背後涉及大量臨時物件建立和銷燬的動作,比如計算圓周率:
>>> area = pi * r ** 2
該語句首先計算r**2,即半徑的平方,中間結果由一個臨時物件來儲存,假如是變數a,然後計算圓周率pi和a的乘積,將最後的結果賦值給變數area,最後,銷燬臨時物件a。
可見這樣一條簡單的浮點運算就隱藏了一個臨時物件的建立和銷燬,如果是複雜的資料運算將涉及大量的物件的建立和銷燬,而這就意味著大量的記憶體分配和回收操作,這是及其耗效能的。
Python考慮了這種情況,在銷燬浮點物件後,並沒有立刻回收記憶體,而是將物件放入一個空閒連結串列中,後續需要建立浮點物件時,可以先從空閒連結串列中取,省去了分配記憶體的開銷。
在檔案Objects/floatobject.c中可以看到浮點物件空間連結串列的定義:
#ifndef PyFloat_MAXFREELIST
#define PyFloat_MAXFREELIST 100
#endif
static int numfree = 0;
static PyFloatObject *free_list = NULL;
- free_list變數,指向空閒連結串列頭節點的指標;
- numfree變數,維護空閒連結串列 當前長度;
- PyFloat_MAXFREELIST巨集,限制空閒連結串列的最大長度,避免佔用過多的記憶體;
為了不新增額外的連結串列指標,free_list把ob_type欄位當做next指標來用,將空閒物件串成連結串列;
以PyFloat_FromDouble為例:
PyFloatObject *op = free_list;
if (op != NULL) {
free_list = (PyFloatObject *) Py_TYPE(op);
numfree--;
} else {
op = (PyFloatObject*) PyObject_MALLOC(sizeof(PyFloatObject));
// ...
}
分配記憶體的流程如下:
- 檢查free_list是否為空;
- 如果free_list非空,則取出頭節點備用,並將numfree減一,並通過Py_TYPE函式(獲取物件的型別物件)取出free_list頭部的ob_type欄位(即第二個空閒物件的地址),將free_list指標指向新的頭部;
- 如果free_list為空,則呼叫PyObject_MALLOC分配記憶體。
如此,每當需要建立浮點物件時,可以從連結串列中取出空閒物件,省去申請記憶體的開銷。而當float物件被銷燬時,Python將其快取在空閒連結串列中,以備後用,程式碼如下:
if (numfree >= PyFloat_MAXFREELIST) {
PyObject_FREE(op);
return;
}
numfree++;
Py_TYPE(op) = (struct _typeobject *)free_list;
free_list = op;
主要流程便是判斷空閒連結串列長度是否達到了限制值,如果達到了,則直接回收物件記憶體,如果未達到,則將物件插到空閒連結串列頭部,並使得numfree加一。
以上部分便是Python空閒物件快取池的介紹,該機制對提高物件分配效率發揮著很重要的作用。