python源码剖析No.4--永不溢出的int对象

引例

1
2
3
4
5
6
7
#include <stdio.h>
int main()
{
int i = 1000000000000000000000;
print(i);
return 0;
}

上述C代码在运行会报错,这是由于C语言的int整型位数位32bit,表示范围有限32 位的有符号整数_C++中整数的范围(-2^31 ~ 2^31-1,-2147483648~2147483647),显然上述代码超出了这个表示范围,因此报错。

相比之下python中却不会出现上述的问题,并且能够正常表示数值。

1
2
3
4
5
6
7
i = 10000000000000000000000000000000000000000
print(i)
print(type(i))

#--------output-------------#
10000000000000000000000000000000000000000
<class 'int'>

更夸张的例子,计算10的100次方

1
2
3
print(10**1000)
#----------output--------------#
10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000

为什么python能做到这一点呢? 这是本节探讨的主题:int对象,永不溢出的整数。


PyLongObject

为了解答上述问题,我们需要从python中int类型对象的设计出发:

python中int对象PyLongObject定义位置:/objects/longobject.h

1
typedef struct _longobject PyLongObject; /* Revealed in longintrepr.h */

跟踪 _longobject来到/include/intrepr.h

1
2
3
4
struct _longobject {
PyObject_VAR_HEAD
digit ob_digit[1];
};

可以看到整型对象PyLongObject根据其对象头部PyObject_VAR_HEAD可以看到是属于变长对象,其次PyLongObject的值是由数组存放的digit ob_digit[1]显然到这里会好奇digit究竟是什么,结果同文件下发现定义,这里只关注相关内容:

1
2
3
4
#if PYLONG_BITS_IN_DIGIT == 30
typedef uint32_t digit;
#elif PYLONG_BITS_IN_DIGIT == 15
typedef unsigned short digit;

从源码得知digit就是一个C语言无符号整数,只是根据位数不同声明的长度不同而已,编译python解析器时可以通过相关宏定义PYLONG_BITS_IN_DIGITPYLONG_BITS_IN_DIGIT来选择使用的digit版本,但默认使用PYLONG_BITS_IN_DIGIT == 30,指示digit数组的长度

至此,我们得知了python利用整型数组来实现更大整数的表示与运算的


这里我们假设digit的元素个数由n,那么python中的int整型对象的布局大致如下:


大整数的布局

大整数分为正数负数,python据此规定不同整数在int对象中的存储方式,总结有以下三点:

  1. 整数绝对值根据值的大小具体情况分为若干部分,保存于ob_digit数组中
  2. ob_digit数组长度保存于PyVarObject.ob_size字段中,对于负整数的情况,PyVarObject.ob_size为负,即ob_size字段不仅表示需要几个数组元素存放int值的绝对值,还表示正负
  3. 整数0PyVarObject.ob_size=0来表示,ob_digit数组为空

实例说明:

2 ** 30是由于上文提到的宏定义PYLONG_BITS_IN_DIGIT相关,为什么不32bit全都使用呢?

这一点其实跟加法进位有关。如果使用全32位用来保存绝对值,那么为了保证加法不溢出(因为加法可能产生进位),需要先强转32位类型为64位,然后进行计算。但牺牲最高1位之后,加法运算便不用担心加法溢出了。

那么为什么多牺牲1位的空间呢?

专栏主认为是为了和digit 只有16位的情况整合起来,当digit数组位数有16位,则python使用15位。下面是我自己的示意图(个人理解)


小整数静态对象池

前面学习过python对对象进行了分类,分为:变长对象 & 定长对象可变对象 & 不可变对象

前面对变长对象 定长对象讨论的比较多,可能有的读者都忘记了可变对象 & 不可变对象的类别了,这里重复一下:

  • 不可变对象,对象在完成初始化后不可做值的修改,强行变动会改变变量名的指向对象。以本节说明的int对象为例,它就是不可变对象:

    1
    2
    3
    4
    5
    6
    7
    a = 1
    print(id(a))
    a += 1
    print(id(a))
    #---------------output---------------#
    4402723168
    4402723200

    可以看到,对不可变对象做修改,会导致变量名指向的对象发生变化

  • 可变对象,以list为例子:

    1
    2
    3
    4
    5
    6
    7
    l = [1,2,3]
    print(id(l))
    l.append(4)
    print(id(l))
    #---------------output---------------#
    140253038051904
    140253038051904

    对可变对象做修改,会导致变量名指向的对象不发生变化

显然对于这类操作是相当频繁的,这会给运行带来极大的开销(一旦不可变对象发生变动,则需要进行新对象的创建,使相对应的变量名指向新的对象,旧的对象若没有被引用还涉及旧对象的销毁!)

那么Python为啥做这种不明智的设计呢?这就不得不提 —- 小整数静态对象池


这是python为解决上述提到的缺陷的方案:预先将常用的整数对象创建好,以备使用,这就是小整数对象池,很简单对吧!相关代码(位置/Objects/longobject.c):

1
2
3
4
5
6
#ifndef NSMALLPOSINTS
#define NSMALLPOSINTS 257
#endif
#ifndef NSMALLNEGINTS
#define NSMALLNEGINTS 5
#endif
  • NSMALLPOSINTS宏规定了对象池正整数个数(包括0),个数=257
  • NSMALLNEGINTS宏规定了对象池负整数个数,个数 = 5

按照默认配置,python启动后静态创建一个包含262个元素的整数数组并一次初始化为负数集合[-5,-1],[0]以及[1,256]相关文章

It turns out Python keeps an array of integer objects for “all integers between -5 and 256”. When we create an int in that range, we’re actually just getting a reference to the existing object in memory.

If we set x = 42, we are actually performing a search in the integer block for the value in the range -5 to +257. Once x falls out of the scope of this range, it will be garbage collected (destroyed) and be an entirely different object. The process of creating a new integer object and then destroying it immediately creates a lot of useless calculation cycles, so Python preallocated a range of commonly used integers.


精彩面试题

1
2
3
4
5
6
a = 1
b = 2-1
print(id(a),'\n',id(b))
#-----------------output-------------#
4402723168
4402723168

其实a, b指向的都是小整数静态对象池中的对象。


回顾

  • python的整数对象是变长 & 不可变对象 能够串联多个C整数类型,实现大整数表示。整数对象的关键字段包括存放对象绝对值的ob_digit数组,存放串联个数以及符号的ob_size
  • 整数绝对值根据PYLONG_BITS_IN_DIGIT指定的ob_digit的数组元素长度进行存放
  • 整数0,digit数组为空,ob_size字段为0
  • 另外,小整数缓冲池优化了不可变对象所带来开销大的问题
Author: Victory+
Link: https://cvjark.github.io/2022/05/16/python源码剖析-永不溢出的int对象/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.