松直,Python中文社区专栏作者
专栏地址:
http://www.zhihu.com/people/songzhili?utm_source=qq&utm_medium=social
在《Python源码剖析》中,Python的版本为2.5,而在Python3中,前面提到,int类型的底层实现是Python2中的long类型。所以,我会在本章中,先介绍Python2源码中int类型的实现,再在最后介绍一下Python3.6中int(也就是以前的long)在底层的实现。之所以这样做的原因后面会解释。
在此之前,我们先来看一个有趣但没什么卵用的现象:
想知道为什么?看完这章就知道了。
初识PyIntObject对象
Python中对整数的概念的实现是通过PyIntObject来完成的。我们曾经说过它是一个定长对象,与之相对的还有变长对象。这样的分法对我们理解Python源码有帮助,但在Python语言的层面上,我们通常还使用一种二分法,即根据对象维护数据的可变性将对象分为可变对象(mutable)和不可变对象(immutable)。Python2中的PyIntObject是一个定长对象,而PyLongObject是一个变长对象,但它们都是不可变对象。也就是说,一旦创建了它们之后,就不能改变它们的值了。
我们来看一下在Python2.7中,PyIntObject的实现:
Python2中的整数对象PyIntObject实际上就是对C原生类型long的一个包装。
我们知道关于一个Python对象的大多数元信息是保存在它的类型对象中的,对于PyIntObject是PyInt_Type:
PyInt_Type中保存了关于PyIntObject的许多元信息,它定义了一个PyIntObject支持的操作,占用的内存大小等等。其中重要的内容我们会在下面叙述。
PyIntObject的创建和维护
创建对象的途径
在intobject.h中,Python提供了几种创建PyIntObject的途径:
事实上,前两种方法都是先将字符串转换为浮点数,再调用PyInt_FromFloat来实现的。
小整数对象
我们来思考一下,在Python内部,整数对象是如此广泛地被使用,尤其是那些比较小的整数。短短几秒之间,我们可能就要用的它们成千上万次,如果我们在创建它们的时候使用malloc来请求分配内存,删除它们时再调用free来释放内存。显然,这样的性能水平是不可能达到我们的要求的,而且也会造成极大的不必要的浪费。
于是,在Python内部,对于小整数使用了对象池技术。
我们可以看出,在Python2.7中,“小整数”的定义是[-5,256],而这个指向一个整数对象数组的指针small_ints就是这个对象池机制的核心。如果我们想要重新定义“小整数”怎么办?简单,我们可以修改源代码并重新编译。
对于小整数对象,Python直接把它们缓存在小整数对象池中,用于共享。那么大整数呢?肯定不可能都缓存在内存中,但是说不定某些大整数在某个时刻会变得十分常用,不过谁也不知道究竟是哪个数字。这时候,Python选择了另一种策略。
大整数对象
Python的设计者的策略是:对于小整数对象,直接把它们全部缓存在对象池中。对于其他整数,Python运行环境将会提供一块内存空间,这块内存空间由这些对象轮流使用。
在Python中,有一个PyIntBlock结构,它被用来实现这个机制。
PyIntBlock的单向列表通过block_list维护,每一个block中都维护了一个PyIntObject数组——objects,这就是真正用于存储被缓存的PyIntObject对象的内存。我们可以想象,在运行的某个时刻,这块内存中一定有一部分被使用,而有一部分是空闲的。这些空闲状态的内存需要被组织起来,以供Python在需要存储新的整数对象时使用。Python使用一个单向链表来管理全部block的objects中的空闲内存,这个链表的表头就是free_list。在一开始,block_list和free_list都指向NULL。
添加和删除
我们来看一下PyIntObject是如何从无到有地产生,又是如何消失的。
使用小整数对象池
如果NSMALLNEGINTS + NSMALLPOSINTS > 0成立,说明小整数对象池机制被激活了,然后Python会检查传入的long值是否是小整数。如果是,就直接返回对象池中的小整数对象就可以了。如果不是,那么会转向通用整数对象池。Python会在block的objects中寻找一块可用于存储新的PyIntObject的内存,这个任务需要free_list来完成。
创建通用整数对象池
显然,当首次调用PyInt_FromLong时,free_list必定为NULL,这时Python会调用fill_free_list创建新的block。在Python运行期间,只要所有block的空闲内存被使用完了,也就是free_list指向NULL,那么下一次调用PyInt_FromLong时就会再次激活对fill_free_list的调用。
在这个函数中,会首先申请一个新的PyIntBlock结构,这时block中的objects还只是一个PyIntObject对象数组。接下来,Python将objects中的所有PyIntObject对象通过指针依次连接起来从而将数组转变成一个单向链表。z在整个链接过程中,Python使用PyObject的ob_type指针作为连接指针。当链表转换完成后,free_list也就出现在出现在它该出现的位置了。从free_list开始,沿着ob_type指针,就可以遍历刚刚创建的所有为PyIntObject准备的内存了。
说完了PyIntObject的创建,我们再来看看它的删除。在Python中,当一个对象引用计数变为0 时,Python就会着手将这个对象销毁。不同类型的对象在销毁时执行的动作也是不同的,其在与对象对应的类型对象中被定义——也就是tp_dealloc。
首先,Python会检查传入的对象是否真的是一个PyIntObject对象,如果是的话,那么将其链入free_list所维护的自由内存链表中,以供将来别的PyIntObject使用。如果只是整数对象的派生类型,那么简单地调用派生类型中指定的tp_free。
使用通用整数对象池
在Python运行的过程中,会不只有一个PyIntBlock存在于同一个链表中,但是它们维护的objects却是分离的,之间没有联系。我们设想一下,有两个PyIntBlock对象,PyIntBlock1和PyIntBlock2,前者已经被填满,后者还有空闲的空间。所以此时free_list指向的是PyIntBlock2.objects中的空闲的内存块。当前者维护的objects中有PyIntObject被删除了,这时前者出现了一块空闲的内存。那么下次创建新的PyIntObject对象时应该使用这块空闲内存。否则就意味着所有的内存只能使用一次,这和内存泄漏没什么两样。
怎么才能将空闲的内存再交由Python使用呢?关键就在于前面我们分析的PyIntObject的删除操作,通过int_dealloc中的操作,所有的PyIntBlock的objects中的空闲内存块都被链接在一起了。它们形成了一个单向链表,表头正是free_list。
小整数对象池的初始化
现在关于Python的整数对象机制还剩最后一个问题。小整数对象池是在什么时候被初始化的呢?
我们可以看到,通过_PyInt_Init的调用,Python创建了这些小整数对象,然后它们就会在整个运行周期中一直存在,直至解释器毁灭。
Python3中int的实现
int即long
我们在之前提到,在Python3中int底层实现就是以前Python2中的long类型。空口无凭,我们来看代码:
我们可以看到,PyLong_Type类型对象的tp_name就是int,也就是说,在Python内部,它就是int类型。
之所以我们在一开始不介绍Python3中的整数实现,是因为在Python3中没有了通用的整数对象池(至少我没有找到),不过还保留着小整数对象池。同时对于那些比较小也就是对于之前Python2中的long类型大材小用的整数来说,也有一个更加快速且节省资源的创建方式。
而且我还发现了一个彩蛋,就是在longobject.c中第二行的注释:
哈哈哈,官方吐槽,最为致命。而且这个注释在Python2.7版本中也有,看来是一段陈年往事。
本文为作者原创作品,未经作者授权同意禁止转载
长按扫描关注Python中文社区,
获取更多技术干货!
Python 中 文 社 区
Python中文开发者的精神家园
合作、投稿请联系微信:
pythonpost
— 人生苦短,我用Python —
1MEwnaxmMz7BPTYzBdj751DPyHWikNoeFS
掌握全球区块链技术最新动态
最专业的数字货币行情分析
— 人生苦短,我用Pyton —
1MEwnaxmMz7BPTYzBdj751DPyHWik