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));
    // ...
}

分配内存的流程如下:

  1. 检查free_list是否为空;
  2. 如果free_list非空,则取出头节点备用,并将numfree减一,并通过Py_TYPE函数(获取对象的类型对象)取出free_list头部的ob_type字段(即第二个空闲对象的地址),将free_list指针指向新的头部;
  3. 如果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空闲对象缓存池的介绍,该机制对提高对象分配效率发挥着很重要的作用。