0%

[python源码分析] 3.float解析

内部结构#

float 实例对象在 Include/floatobject.h 中定义如下:

1
2
3
4
typedef struct {
PyObject_HEAD //定长对象共用的头部
double ob_fval; //额外字段,存储对象所承载的浮点值
} PyFloatObject;

下面是浮点实例对象内部结构图:

float 类型对象#

与实例对象不同, float 类型对象 全局唯一 ,因此可以作为 全局变量 定义。 在 C 文件 Objects/floatobject.c 中,我们找到了代表 float 类型对象的全局变量 PyFloat_Type

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
PyTypeObject PyFloat_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"float", /*tp_name 字段保存类型名称,常量 float*/
sizeof(PyFloatObject),
0,
(destructor)float_dealloc, /* tp_dealloc 对象销毁相关*/
0, /* tp_print */
0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_reserved */
(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 */
(reprfunc)float_repr, /* tp_str 生成普通字符串表示形式的函数*/
PyObject_GenericGetAttr, /* tp_getattro */
0, /* tp_setattro */
0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* 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 对象创建相关(tp_init 函数指针在这为空,因为float对象简单,只需要tp_new赋值就行)*/
0, /* tp_alloc 对象创建相关*/
float_new, /* tp_new 对象创建相关*/
};

PyFloat_Type 中保存了很多关于浮点对象的 元信息

PyFloat_Type 很重要,作为浮点 类型对象 ,它决定了浮点 实例对象 的 生死和行为

对象的创建#

调用类型对象 float 创建实例对象: Python 执行的是 type 类型对象中的 tp_call 函数。 tp_call 函数进而调用 float 类型对象的 tp_new 函数创建实例对象, 再调用 tp_init 函数对其进行初始化

除了通用的流程, Python 为内置对象实现了对象创建 API ,简化调用,提高效率:

1
2
3
4
5
PyObject *
PyFloat_FromDouble(double fval); /*通过浮点值创建浮点对象*/

PyObject *
PyFloat_FromString(PyObject *v); /*通过字符串对象创建浮点对象*/

PyFloat_FromDouble 为例,特化的对象创建流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
PyObject *
PyFloat_FromDouble(double fval)
{
PyFloatObject *op = free_list;
/*为对象 分配内存空间,优先使用空闲对象缓存池 */
if (op != NULL) {
free_list = (PyFloatObject *) Py_TYPE(op);
numfree--;
}
/*空闲对象缓存池为空,调用PyObject_MALLOC申请内存 */
else {
op = (PyFloatObject*) PyObject_MALLOC(sizeof(PyFloatObject));
if (!op)
return PyErr_NoMemory();
}
/* Inline PyObject_New */
(void)PyObject_INIT(op, &PyFloat_Type);

/*将 ob_fval 字段初始化为指定 浮点值 */
op->ob_fval = fval;
return (PyObject *) op;
}

上面用到的宏 PyObject_INIT 在头文件 Include/objimpl.h 中定义为:

1
2
#define PyObject_INIT(op, typeobj) \
( Py_TYPE(op) = (typeobj), _Py_NewReference((PyObject *)(op)), (op) ) //前半部分调用 Py_TYPE(op) 初始化 对象类型 字段 ob_type,后面语句初始化 引用计数 字段 ob_refcnt

上面提到的宏定义 Py_TYPE,位于 Include/object.h 头文件:

1
#define Py_TYPE(ob) (((PyObject*)(ob))&ob_type)
它的作用是:将 给定对象的类型对象取出, 返回对象的ob_type字段

宏 _Py_NewReference,在 Include/Object.h 中定义:

1
2
3
4
#define _Py_NewReference(op) (                          \
_Py_INC_TPALLOCS(op) _Py_COUNT_ALLOCS_COMMA \
_Py_INC_REFTOTAL _Py_REF_DEBUG_COMMA \
Py_REFCNT(op) = 1) /*将对象引用计数初始化为 1*/

# 对象的销毁 当对象不再需要时, Python 通过 Py_DECREF 或者 Py_XDECREF 宏减少引用计数; 当引用计数降为 0 时, Python 通过 **_Py_Dealloc** 宏回收对象:
1
2
3
#define _Py_Dealloc(op) (                               \
_Py_INC_TPFREES(op) _Py_COUNT_ALLOCS_COMMA \
(*Py_TYPE(op)->tp_dealloc)((PyObject *)(op))) /*调用类型对象 **PyFloat_Type 中的 tp_dealloc 函数指针*/
因此,实际调用的函数是 float_dealloc (代码在下一小节 空闲对象缓存池 中解析):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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);
}
总结起来,对象从创建到销毁整个生命周期所涉及的 关键函数、宏及调用关系 如下:

空闲对象缓存池#

浮点运算背后涉及 大量临时对象创建以及销毁 ,以下面计算为例:

1
>>> area = pi * r ** 2
这个语句首先计算半径 r ** 2,中间结果由一个临时对象来保存,假设是 t ; 然后计算圆周率 pi 与 t 的乘积,得到最终结果并赋值给变量 area ; 最后,销毁临时对象 t 。 这么简单的语句,背后居然都隐藏着一个 临时对象的创建以及销毁操作!

创建对象时需要分配内存,销毁对象时又需要回收内存。 大量临时对象创建销毁 ,意味着 大量内存分配回收操作 ,这显然是是不可接受的。

因此 Python 在浮点对象销毁后,并不急于回收内存,而是将对象放入一个 空闲链表 。 后续需要创建浮点对象时,先到空闲链表中取,省去分配内存的开销。

浮点对象的空闲链表同样在 Objects/floatobject.c 中定义:
1
2
3
4
5
#ifndef PyFloat_MAXFREELIST
#define PyFloat_MAXFREELIST 100 /*该宏 限制空闲链表的 最大长度 ,避免占用过多内存*/
#endif
static int numfree = 0; /*维护空闲链表 当前长度*/
static PyFloatObject *free_list = NULL; /*指向空闲链表 头节点 的指针*/
为了保持简洁, Python 把 ob_type 字段当作 next 指针来用,将 空闲对象串成链表

因此创建浮点对象时,可以从 链表中取出空闲对象,省去 申请内存的开销! 以 PyFloat_FromDouble 为例:

1
2
3
4
5
6
7
8
PyFloatObject *op = free_list;                  /*op指向第一个 空闲对象*/
if (op != NULL) {
free_list = (PyFloatObject *) Py_TYPE(op); /*free_list指向第一个 空闲对象(op)的ob_type所指向的 下一个空闲对象(相当于链表的头部删除)*/
numfree--; /*更新空闲链表维护的数量*/
} else {
op = (PyFloatObject*) PyObject_MALLOC(sizeof(PyFloatObject)); /*free_list为空时,重新分配内存*/
// ...
}

对象销毁时, Python 将其缓存在空闲链表中,以备后用。考察 float_dealloc 函数:

1
2
3
4
5
6
7
if (numfree >= PyFloat_MAXFREELIST)  {   /*销毁时,判断free_list是否达到最大容量*/
PyObject_FREE(op); /*回收对象内*/
return;
}
numfree++;
Py_TYPE(op) = (struct _typeobject *)free_list; /*op的ob_type指向当前 第一个空闲对象*/
free_list = op; /* free_list指向op(相当于链表的头部插入)*/
空闲对象缓存池在 提高对象分配效率 方面发挥着至关重要的作用。

对象的行为#

PyFloat_Type 中定义了很多函数指针,包括 tp_repr 、 tp_str 、 tp_hash 等。 这些函数指针将一起决定 float 对象的行为,例如 tp_hash 函数决定浮点哈希值的计算:

1
2
3
>>> pi = 3.14
>>> hash(pi)
322818021289917443
tp_hash 函数指针指向 float_hash 函数,实现了 针对浮点对象的哈希值算法
1
2
3
4
5
static Py_hash_t
float_hash(PyFloatObject *v)
{
return _Py_HashDouble(v->ob_fval);
}
## 数值操作集 由于加减乘除等数值操作很常见, Python 将其抽象成数值操作集 PyNumberMethods 。 数值操作集 PyNumberMethods 在头文件 Include/object.h 中定义:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef struct {
/* Number implementations must check *both*
arguments for proper type and implement the necessary conversions
in the slot functions themselves. */

binaryfunc nb_add;
binaryfunc nb_subtract;
binaryfunc nb_multiply;
binaryfunc nb_remainder;
binaryfunc nb_divmod;
ternaryfunc nb_power;
unaryfunc nb_negative;
// ...

binaryfunc nb_inplace_add;
binaryfunc nb_inplace_subtract;
binaryfunc nb_inplace_multiply;
binaryfunc nb_inplace_remainder;
ternaryfunc nb_inplace_power;
//...
} PyNumberMethods;
PyNumberMethods 定义了各种数学算子的处理函数,数值计算最终由这些函数执行。 处理函数根据参数个数可以分为: 一元函数 ( unaryfunc )、 二元函数 ( binaryfunc )等。

  • 一元函数 ( unaryfunc ): 需要传入一个参数的函数。
  • 二元函数 ( binaryfunc): 需要传入两个参数的函数。

回到 Objects/floatobject.c 观察浮点对象数值操作集 float_as_number 是如何初始化的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static PyNumberMethods float_as_number = {
float_add, /* nb_add */
float_sub, /* nb_subtract */
float_mul, /* nb_multiply */
float_rem, /* nb_remainder */
float_divmod, /* nb_divmod */
float_pow, /* nb_power */
(unaryfunc)float_neg, /* nb_negative */
// ...

0, /* nb_inplace_add */
0, /* nb_inplace_subtract */
0, /* nb_inplace_multiply */
0, /* nb_inplace_remainder */
0, /* nb_inplace_power */
// ...
};
以加法为例,以下语句在 Python 内部最终由 float_add 函数执行:
1
2
3
4
>>> a = 1.5
>>> b = 1.1
>>> a + b
2.6
float_add 是一个 二元函数 ,位于 Objects/floatobject.h 中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static PyObject *
float_add(PyObject *v, PyObject *w)
{
double a,b;
//将两个参数对象转化成浮点值,CONVERT_TO_DOUBLE是一个宏,将PyFloatObject里面的ob_fval抽出来给double变量
CONVERT_TO_DOUBLE(v, a);
CONVERT_TO_DOUBLE(w, b);
PyFPE_START_PROTECT("add", return 0)

//对两个浮点值求和
a = a + b;
PyFPE_END_PROTECT(a)

//创建一个新浮点对象保存计算结果并返回
return PyFloat_FromDouble(a);
}
浮点数计算一般都遵循 IEEE-754标准,如果计算时出现了错误,那么需要 将IEEE-754异常转换成Python中的异常,而 PyFPE_START_PROTECT和PyFPE_END_PROTECT这两个宏就是用来干这件事情的。 它们的定义在Include/pyfpe.h中,并且Python3.9的时候会被删除掉。

所以如果是 C中的两个浮点数相加,直接a + b就可以了,编译之后就是一条简单的机器指令,然而 Python则需要额外做很多其它工作。从一个简单的加法上面就可以看出来Python为什么会比C慢几十倍了。

reference#

  1. 小试牛刀,解剖浮点对象 float
  2. 浮点数的底层实现