Python内部机制-PyIntObject对象

Python int对象的实现

Python内部关于int对象的实现在我之前的两篇文章中其实已经简单的介绍过,本文会前面的基础上更加深入的分析int对象的内部实现,以及Python对int对象进行优化而采用的缓存技术等等.首先还是来看看int对象在C层面的一个数据结构吧.

typedef struct {             
    PyObject_HEAD            
    long ob_ival;            
} PyIntObject; 

Include/intobject.h

PyObject_HEAD是一个宏,这在前面的几篇文章中都详细分析过,所以对于int对象来说最重要的莫过于ob_ival成员了,这个成员用来存放实际存储的数据.有了这个基础我们就可以在Python源码中开始漫游了.先来看看int对象做了哪些初始化动作吧.

int对象的初始化

int                       
_PyInt_Init(void)                                                                                                                                                         
{                         
    PyIntObject *v;       
    int ival;             
#if NSMALLNEGINTS + NSMALLPOSINTS > 0
    for (ival = -NSMALLNEGINTS; ival < NSMALLPOSINTS; ival++) {
          if (!free_list && (free_list = fill_free_list()) == NULL)
                    return 0;
        /* PyObject_New is inlined */
        v = free_list;    
        free_list = (PyIntObject *)Py_TYPE(v);
        PyObject_INIT(v, &PyInt_Type);
        v->ob_ival = ival;
        small_ints[ival + NSMALLNEGINTS] = v; 
    }                     
#endif                    
    return 1;             
} 

Object/intobject.c

这段初始化代码信息量很大,包含了本文即将分析的小整数对象的缓存优化,还有大整数对象池等知识点.这段初始化代码做了两件事,第一件事就是创建大整数对象池,第二件事就是小整数对象的缓存的初始化.具体细节后面分析到相关知识点的时候再具体分析.这里我们重点看一下PyObject_INIT,这是一个宏
专门用于初始化类型对象的.在上面的初始化代码中v是一个PyIntObject类型,但是还没有进行初始化.通过PyObject_INIT宏.将PyIntObject对象和这个对象对应的类型PyInt_Type关联起来.这也是我在前面几篇文章中提到的,对象会在初始化的时候固定其类型.PyInt_Type是一个专门为int对象进行定制的一个PyTypeObject(如果你忘了这个对象是干什么的,请看我之前的文章)对象.

下面是PyObject_INIT宏的具体实现:

#define PyObject_INIT(op, typeobj) \
    ( Py_TYPE(op) = (typeobj), _Py_NewReference((PyObject *)(op)), (op) )

Include/objimpl.h

#define Py_TYPE(ob)             (((PyObject*)(ob))->ob_type)

Include/object.h

PyObject_INIT的实现很简单,就是把PyInt_TypePyIntObjectob_type成员关联起来.这样就完成了PyIntObject对象的初始化了,然后再增加引用计数.到此初始化就结束了,说完了初始化,该说说如何创建一个int对象了吧.

int对象的构造

看一下相关的头文件,发现Python居然提供了这么多的构造函数,可以从字符串,UNICODE,long,size_t,Py_ssize_t等类型来构造一个PyIntObject对象.

PyAPI_FUNC(PyObject *) PyInt_FromString(char*, char**, int);
#ifdef Py_USING_UNICODE
PyAPI_FUNC(PyObject *) PyInt_FromUnicode(Py_UNICODE*, Py_ssize_t, int);
#endif              
PyAPI_FUNC(PyObject *) PyInt_FromLong(long);
PyAPI_FUNC(PyObject *) PyInt_FromSize_t(size_t);
PyAPI_FUNC(PyObject *) PyInt_FromSsize_t(Py_ssize_t);

Include/object.h
当你开始去分析每一个构造函数的时候,你会发现另外一个有意思的地方,那就是这些构造函数最终都会去调用PyInt_FromLong这个函数,构造函数大同小异因此接下来我们通过分析PyInt_FromLong函数的实现来学习如何构造int对象.

PyObject *
PyInt_FromLong(long ival)
{
    register PyIntObject *v;
//Setp1: 查看ival是否在整数范围内,如果在就直接返回缓存的PyIntObject对象
#if NSMALLNEGINTS + NSMALLPOSINTS > 0
    if (-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS) {
        v = small_ints[ival + NSMALLNEGINTS];
        Py_INCREF(v);
#ifdef COUNT_ALLOCS
        if (ival >= 0)
            quick_int_allocs++;
        else 
            quick_neg_int_allocs++;
#endif
        return (PyObject *) v;
    }
#endif
//Setp2: 查看是否有空闲的PyIntObject对象,如果没有调用fill_free_list开始分配一个
    if (free_list == NULL) {
        if ((free_list = fill_free_list()) == NULL)
            return NULL;
    }
//Setp3: 初始化PyIntObject,然后赋值,最后返回.
    /* Inline PyObject_New */
    v = free_list;
    free_list = (PyIntObject *)Py_TYPE(v);
    PyObject_INIT(v, &PyInt_Type);
    v->ob_ival = ival;
    return (PyObject *) v;
}

Object/intobject.c
上面的代码分成了几部分,第一部分就是查看传入的ival是否在小整数的范围内,如果在直接返回缓存的PyIntObject对象,如果不存在就去查看整数对象池中是否有PyIntObject对象如果有则从对象池链表中摘下来返回,如果没有则分配一个,然后初始化,赋值最后返回.但是这段代码远比我说的要复杂多了,下文会详细的介绍.到此为止,一个int对象做了哪些初始化,以及如何构造一个int对象都已经介绍完毕了.我相信大家此时心里至少有了一个全局观,但是对细节的把握肯定还是不够的,上文中提到的小整数对象缓存,大整数对象池都是存疑的.那么下面我将带领大家更加深入的解析Python中的int对象.

小整数对象缓存

大家先来看一段python程序,通过这段python程序可以让我们直观的看到什么是小整数对象缓存机制.

>>> a = 1
>>> b = 1
>>> id(a)
35664216
>>> id(b)
35664216
>>> a = 10000
>>> b = 10000
>>> id(a)
35946744
>>> id(b)
35946624

id命令是用于打印一个对象在内存的虚拟地址的,通过上面的代码你会发现当a和b是1的时候其对象地址是相同的,也就是说两者其实是一个对象,只是名字不同而已,当a和b是10000的时候你又会发现两个对象的地址不同的了,也就是说两者是两个不同的对象.你还可以接着测试a和b为2,3,4…的时候,试图找一找临界点,在什么范围情况下是相同的,什么范围情况下是不同的.看到这里是不是对上面的结果很好奇呢?,这里用到的就是所谓的小整数缓存机制,Python默认对于[-5,257)范围内的整数都是使用的小整数缓存机制,也就是说当int对象的值范围在[-5,257)内其实用的都是全局的已经初始化好的int对象.现在我们回到源码中来验证一下吧.还记得上文说的int对象初始化吗?,没错就是哪里

#if NSMALLNEGINTS + NSMALLPOSINTS > 0
    for (ival = -NSMALLNEGINTS; ival < NSMALLPOSINTS; ival++) {
          if (!free_list && (free_list = fill_free_list()) == NULL)
                    return 0;
        /* PyObject_New is inlined */
        v = free_list;    
        free_list = (PyIntObject *)Py_TYPE(v);
        PyObject_INIT(v, &PyInt_Type);
        v->ob_ival = ival;
        small_ints[ival + NSMALLNEGINTS] = v; 
    }                     
#endif         

Object/intobject.c

#ifndef NSMALLPOSINTS
#define NSMALLPOSINTS           257
#endif    
#ifndef NSMALLNEGINTS
#define NSMALLNEGINTS           5
#endif    
#if NSMALLNEGINTS + NSMALLPOSINTS > 0
/* References to small integers are saved in this array so that they
   can be shared.
   The integers that are saved are those in the range
   -NSMALLNEGINTS (inclusive) to NSMALLPOSINTS (not inclusive).
*/        
static PyIntObject *small_ints[NSMALLNEGINTS + NSMALLPOSINTS];
#endif

Object/intobject.c

NSMALLNEGINTS是5,NSMALLPOSINTS是257,small_ints是一个大小是NSMALLNEGINTS + NSMALLPOSINTSPyIntObject类型的数组上面的初始化代码首先是一个for循环,从-NSMALLNEGINTS遍历到NSMALLPOSINTS,每次遍历的时候通过 fill_free_list()创建了一个PyIntObject对象
然后使用v指向这个刚创建的PyIntObject对象v = free_list;,接着初始化这个PyIntObject对象PyObject_INIT(v, &PyInt_Type);v->ob_ival = ival;最后用v来初始化全局的对象数组small_ints[ival + NSMALLNEGINTS] = v;读到这里我相信大家已经对小整数的缓存机制有了一定的了解了吧.现在回头看看int对象构造那个章节结合小整数缓存这个章节,你是不是应该对下面的代码有了更深入的理解了啊.

//Setp1: 查看ival是否在整数范围内,如果在就直接返回缓存的PyIntObject对象
#if NSMALLNEGINTS + NSMALLPOSINTS > 0
    if (-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS) {
        v = small_ints[ival + NSMALLNEGINTS];
        Py_INCREF(v);
#ifdef COUNT_ALLOCS
        if (ival >= 0)
            quick_int_allocs++;
        else 
            quick_neg_int_allocs++;
#endif
        return (PyObject *) v;
    }
#endif

至于这个章节中提到的free_listfill_free_list()等,这是本文即将要讨论的另外一个知识点也就是大整数缓存机制,这里占且不讨论,继续往下看.

大整数对象池

从前面的文章中我们已经知道Python中处处都是对象,而对象在C语言层面其实就是在堆上分配的一堆结构体.大量的对象构造和释放会导致大量的内存申请和释放,这势必会导致内存碎片的问题,Python中为了避免内存碎片的问题引入了对象池的,也就是内存释放的时候不再归还给系统,而是放到对象池中.接下来我们来看看Python中对象池是如何实现的吧.

struct _intblock {                                           
    struct _intblock *next;                                  
    PyIntObject objects[N_INTOBJECTS];                       
};                                                           

typedef struct _intblock PyIntBlock; 

一个对象池就是一个PyIntBlock类型的单链表.一个PyIntBlock可以存放N_INTOBJECTS个整数对象.初始的时候这个单链表是空的.

static PyIntBlock *block_list = NULL;
static PyIntObject *free_list = NULL;

block_list指向PyIntBlock单链表中的第一个PyIntBlock,初始的时候这个单链表是空的,那么当需要使用对象池的时候就会调用fill_free_list来创建这个对象池.接下来我们来看看fill_free_list是如何创建的吧.

static PyIntObject *                   
fill_free_list(void)                   
{                                      
    PyIntObject *p, *q;                 
    /* Python's object allocator isn't appropriate for large blocks. */
    //Setp1: 分配一个sizeof(PyIntBlock)大小的内存.
    p = (PyIntObject *) PyMem_MALLOC(sizeof(PyIntBlock));
    //处理分配失败的情况
    if (p == NULL)                      
        return (PyIntObject *) PyErr_NoMemory();
    //Setp2:头插法插入block_list指向的单链表
    ((PyIntBlock *)p)->next = block_list;
    //block_list重新指向新的单链表头
    block_list = (PyIntBlock *)p;                        
    /* Link the int objects together, from rear to front, then return
       the address of the last int object in the block. */
    p = &((PyIntBlock *)p)->objects[0];                                                                                                                   
    q = p + N_INTOBJECTS;
    //setp3: 这一步很巧妙,将PyIntBlock中的objects数组变成了单链表,通过拿前一个PyIntObject对象的ob_type成员指向后一个PyIntObject对象的ob_type成员实现 
    //这里和linux内核中的单链表实现的思想是如出一辙的.
    while (--q > p)                    
        Py_TYPE(q) = (struct _typeobject *)(q-1); //struct _typeobject就是PyTypeObject,Py_TYPE就是取q的ob_type成员
    Py_TYPE(q) = NULL; 
    //setp3: 最后返回PyIntBlock中的objects数组的最后一个成员.             
    return p + N_INTOBJECTS - 1;       
} 

通过fill_free_list可以创建PyIntBlock,然后返回其中的objects数组的最后一个元素.那么我们再来看看如何使用PyIntBlock吧.还记得上文中说的int对象初始化和int对象的构造吗?,这两个地方都用到了.

int
_PyInt_Init(void)
{
    PyIntObject *v;
    int ival;
#if NSMALLNEGINTS + NSMALLPOSINTS > 0
    for (ival = -NSMALLNEGINTS; ival < NSMALLPOSINTS; ival++) {
          if (!free_list && (free_list = fill_free_list()) == NULL)
                    return 0;
        /* PyObject_New is inlined */
        v = free_list;
        free_list = (PyIntObject *)Py_TYPE(v);
        PyObject_INIT(v, &PyInt_Type);
        v->ob_ival = ival;
        small_ints[ival + NSMALLNEGINTS] = v; 
    }
#endif
    return 1;
} 

这段初始化代码.不光光初始化了小整数缓存,还创建了一个PyIntBlock,然后使用PyIntBlock中的objects数组来初始化小整数缓存.初始的时候free_list是空,所以所有的for循环中,只有第一次循环的时候才会通过fill_free_list分配一个PyIntBlock对象,然后free_list指向PyIntBlock中的objects数组中的最后一个,接着通过(PyIntObject *)Py_TYPE(v)指向下一个PyIntObject对象.这里是初始化的时候用到了PyIntBlock,那么我们来看看在构造的时候是如何使用PyIntBlock对象的.

PyObject *     
PyInt_FromLong(long ival)
{                                                                                                                                                                             
    register PyIntObject *v;
#if NSMALLNEGINTS + NSMALLPOSINTS > 0
    if (-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS) {
        v = small_ints[ival + NSMALLNEGINTS];
        Py_INCREF(v);
#ifdef COUNT_ALLOCS
        if (ival >= 0)
            quick_int_allocs++;
        else   
            quick_neg_int_allocs++;
#endif         
        return (PyObject *) v;
    }          
#endif         
    if (free_list == NULL) {
        if ((free_list = fill_free_list()) == NULL)
            return NULL;
    }          
    /* Inline PyObject_New */
    v = free_list;
    free_list = (PyIntObject *)Py_TYPE(v);
    PyObject_INIT(v, &PyInt_Type);
    v->ob_ival = ival;
    return (PyObject *) v;
}  

如果是小整数那么直接从小整数缓存中分配一个返回,如果不是那么首先看看free_list是否为空,如果为空说明PyIntBlock已经用完了,那么使用fill_free_list分配一个然后初始化返回.代码还是很容易看懂的.到此为止int对象相关的知识点都已经说完了.

©️2020 CSDN 皮肤主题: 编程工作室 设计师: CSDN官方博客 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值