Python缓存整数
整数,在Python中,不是以传统的2、4或8字节实现,而是将其实现为以230为基数的整数数组,因此Python支持超长整数。又因为整数没有明确的长度限制,所以在Python中使用整数非常方便,即使我们对很长的整数进行运算,也不必担心整数溢出。但这种便利的代价是资源分配较高,同时常用运算(例如加法,乘法,除法)的执行效率较低。
Python中的每个整数都实现为一个C语言结构体, 如下所示。
可以观察到,与其它的较长整数相比,-5至256范围内的较小整数使用非常频繁,为了性能上优势,Python在初始化过程中预先分配了该范围内的整数对象,并使它们成为单例,因此每次在使用较小整数时, 使用的是相应单例的引用,而不是重新分配新的整数对象。
下面是Python官方文档中关于整数预分配的相关内容:
“当前的整数实现会为-5至256之间的所有整数维护一个整数对象数组,当你创建该范围内的整数时, 你实际得到的是现有整数对象的引用。”
在CPython的源码中,可以在IS_SMALL_INT宏和longobject.c模块中的get_small_int函数中跟踪此优化。因此,Python为常用整数节省了大量的空间和计算量。
验证这些较小整数对象是否是单例
对于CPython, 内置的id函数返回对象的内存地址。这意味着,如果较小整数确实是单例,对于相同较小整数值的两个实例, id函数应该返回相同的内存地址,而对于较长整数的多个实例应返回不同的内存地址,这确实是我们观察到的:
较小整数的这种单例现象也可以在计算过程中观察到。在下面的示例中,通过对三个不同的整数2、4和10进行两次运算,我们得到了相同的目标值6,并且在这两种情况下,我们看到id函数返回了相同的内存地址:
验证这些较小整数对象是否经常被引用
我们已经确定,在Python中使用较小整数,是通过引用它们相应的单例对象来实现,而无需每次都重新分配内存创建整数对象。现在我们需要验证以下假设:“Python初始化的时候, 通过这些单例对象,确实节省了大量的内存分配操作”;我们可以通过检查每个整数值的引用计数来验证上面的假设。
引用计数
引用计数,维护着同一对象在不同位置被引用的数目,对象每次被引用时,其属性ob_refcnt(对象引用计数)会增加1,对象引用被取消时, ob_refcnt会减少1,当引用计数为0时,该对象将会被垃圾回收。
我们使用sys模块中的getrefcount函数来获取一个对象的当前引用计数;
当对-5至300之间的所有整数执行此操作时,我们得到以下分布:
上图显示,较小整数的引用计数很高,表明使用率很高,同时引用计数随着这些整数值的增加而降低;这证实了, 在Python初始化期间, 相比于较长整数,较小整数被更多对象引用。
整数值0被引用的最多——359次,沿着分布图的长尾,可以看到引用计数的峰值为2的幂次,如32, 64, 128和256。Python在初始化的过程中本身需要较小整数值,因此通过单例模式节省了大约1993次的较小整数对象的创建。
这些引用计数是在新打开的Python进程计算的,这意味着,Python初始化时,需要使用一些整数值进行计算,并通过使用较小整数值的单例来加速计算。
在通常的编程实践中, 相比于较长整数,较小整数的使用更频繁,并且这些整数值的单例实例可以为Python节省大量的计算量和内存分配操作。
参考:
Python Object Types and Reference Counts
How python implements super-long integers
Why Python is Slow: Looking Under the Hood
英文原文:https://arpitbhayani.me/blogs/python-caches-integers
译者:张恒源