「Python 整数对象」:探索 Python 内部整数值的存储和计算

517 阅读4分钟

Python3只保留了PyLongObject,我看的版本为3.11

结构体

源文件:Include/pytypedefs.h

typedef struct _longobject PyLongObject;

源文件:Include/cpython/longintrepr.h

struct _longobject {
    PyObject_VAR_HEAD
    digit ob_digit[1];
};

使用了不定长对象头部,可以知道PyLongObject为变长对象。还有一个digit数组

typedef uint32_t digit;

可以看出,int对象是通过整数数组来实现的。但ob_digit数组的长度为1,这是为啥呢?翻阅资料得知由于 C 语言中数组长度不是类型信息,我们可以根据实际需要为 ob_digit 数组分配足够的内存,并将其当成长度为 n 的数组操作。这也是 C 语言中一个常用的编程技巧。

类型对象

源文件:Objects/longobject.c

PyTypeObject PyLong_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "int",                                      /* tp_name */
    offsetof(PyLongObject, ob_digit),           /* tp_basicsize */
    sizeof(digit),                              /* tp_itemsize */
    0,                                          /* tp_dealloc */
    long_to_decimal_string,                     /* tp_repr */  
    &long_as_number,                            /* tp_as_number */
    0,                                          /* tp_as_sequence */
    0,                                          /* tp_as_mapping */
    // ......
    0,                                          /* tp_init */
    0,                                          /* tp_alloc */
    long_new,                                   /* tp_new */
    PyObject_Free,                              /* tp_free */
};

可以看到tp_name为int,创建整数对象调用long_new

创建整数对象

源文件:Objects/clinic/longobject.c.h

static PyObject *
long_new(PyTypeObject *type, PyObject *args, PyObject *kwargs)
{
    PyObject *return_value = NULL;
    static const char * const _keywords[] = {"", "base", NULL};
    static _PyArg_Parser _parser = {NULL, _keywords, "int"0};
    PyObject *argsbuf[2];
    PyObject * const *fastargs;
    Py_ssize_t nargs = PyTuple_GET_SIZE(args);
    Py_ssize_t noptargs = nargs + (kwargs ? PyDict_GET_SIZE(kwargs) : 0) - 0;
    PyObject *x = NULL;
    PyObject *obase = NULL;

    fastargs = _PyArg_UnpackKeywords(_PyTuple_CAST(args)->ob_item, nargs, kwargs, NULL, &_parser, 020, argsbuf);
    if (!fastargs) {
        goto exit;
    }
    if (nargs < 1) {
        goto skip_optional_posonly;
    }
    noptargs--;
    x = fastargs[0];
skip_optional_posonly:
    if (!noptargs) {
        goto skip_optional_pos;
    }
    obase = fastargs[1];
skip_optional_pos:
    return_value = long_new_impl(type, x, obase);

exit:
    return return_value;
}

可以看到具体实现,调用long_new_impl

源文件:Objects/longobject.c

static PyObject *
long_new_impl(PyTypeObject *type, PyObject *x, PyObject *obase)
/*[clinic end generated code: output=e47cfe777ab0f24c input=81c98f418af9eb6f]*/
{
    Py_ssize_t base;

    if (type != &PyLong_Type)
        return long_subtype_new(type, x, obase); /* Wimp out */
    if (x == NULL) {
        if (obase != NULL) {
            PyErr_SetString(PyExc_TypeError,
                            "int() missing string argument");
            return NULL;
        }
        return PyLong_FromLong(0L);
    }
    /* default base and limit, forward to standard implementation */
    if (obase == NULL)
        return PyNumber_Long(x);

    base = PyNumber_AsSsize_t(obase, NULL);
    if (base == -1 && PyErr_Occurred())
        return NULL;
    if ((base != 0 && base < 2) || base > 36) {
        PyErr_SetString(PyExc_ValueError,
                        "int() base must be >= 2 and <= 36, or 0");
        return NULL;
    }

    if (PyUnicode_Check(x))
        return PyLong_FromUnicodeObject(x, (int)base);
    else if (PyByteArray_Check(x) || PyBytes_Check(x)) {
        const char *string;
        if (PyByteArray_Check(x))
            string = PyByteArray_AS_STRING(x);
        else
            string = PyBytes_AS_STRING(x);
        return _PyLong_FromBytes(string, Py_SIZE(x), (int)base);
    }
    else {
        PyErr_SetString(PyExc_TypeError,
                        "int() can't convert non-string with explicit base");
        return NULL;
    }
}

看源码我们可以知道

  • 当x == NULL 且 obase != NULL时返回错误信息
int(base = 0)  // int() missing string argument
  • 当x == NULL 且 obase == NULL时,调用PyLong_FromLong(0L)

  • 当x != NULL obase 为 NULL 调用 PyNumber_Long(x)

  • 当x 和 obase 都不为 NULL

    如果(base != 0 && base < 2) || base > 36, 报错

    int('0b100'base=1// int() base must be >= 2 and <= 36, or 0
    

​ PyUnicode 调用 PyLong_FromUnicodeObject,最终调用 PyLong_FromString

​ PyByteArray/PyBytes 调用_PyLong_FromBytes,最终调用 PyLong_FromString

小整数静态对象池

通过整数对象的结构体,知道整数对象是可变对象,整数运算结果都是以新对象返回的

a = 1
id(a) // 4310199024
a += 1
id(a) // 4310199056

这样如果循环上千次,就会创建上千个对象,会有大量的内存分配、销毁,性能很差。Python设计者当然不允许有这样的缺陷,它预先将常用的整数对象创建好,这就是小整数对象池

我们看看,创建整数对象调用的方法,Objects/longobject.c

PyObject *
PyLong_FromLong(long ival)
{
    PyLongObject *v;
    unsigned long abs_ival, t;
    int ndigits;

    /* Handle small and medium cases. */
    if (IS_SMALL_INT(ival)) {
        return get_small_int((sdigit)ival);
    }
    // ......
    return (PyObject *)v;
}

可以看到会检测是否是小整数

#define IS_SMALL_INT(ival) (-_PY_NSMALLNEGINTS <= (ival) && (ival) < _PY_NSMALLPOSINTS)

源文件:Include/internal/pycore_global_objects.h

#define _PY_NSMALLPOSINTS           257
#define _PY_NSMALLNEGINTS           5

struct _Py_global_objects {
    struct {
        /* Small integers are preallocated in this array so that they
         * can be shared.
         * The integers that are preallocated are those in the range
         * -_PY_NSMALLNEGINTS (inclusive) to _PY_NSMALLPOSINTS (exclusive).
         */
        PyLongObject small_ints[_PY_NSMALLNEGINTS + _PY_NSMALLPOSINTS];

        // ......
};
  • _PY_NSMALLPOSINTS宏规定了对象池正数个数,默认257个
  • _PY_NSMALLNEGINTS宏规定了对象池负数个数,默认5个
  • small_ints 是一个整数对象数组,保存预先创建好的小整数对象

Python启动后静态创建一个包含262个元素的整数数组,并初始化-5、-4、...、0、1、...、256这些整数对象。

Python 整数对象_00.png

到这我们知道,如果是小整数的话,内存地址应该是一样的

a = 2 - 1
id(a) // 4310199024
b = 1
id(b) // 4310199024
c = 0 + 1
id(c) // 4310199024

结果都是1,在小整数范围内,Python会直接从静态对象池中取出整数1。

a = 257
id(a) // 4312383408
b = 256+1
id(b) // 4312383440

这里a和b的结果257,但257不在小整数范围内,Python会分别创建对象返回,因此a和b绑定的对象id也就不同。

但是,喜欢尝试的同学,在Pycharm中运行时,发现id是一样的

a = 257
print(id(a)) // 4327165136
b = 256+1
print(id(b)) // 4327165136

这是为啥呢?查资料得到的结果:本质上是和字节码有关的,在IDLE中,每个命令都会单独编译,而在Pycharm中编译整个py文件。后续我们专门写一篇文章介绍字节码

整数的存储结构

我们可以在使用print输出整数时,输出ob_sizeob_digit。通过类型对象中的tp_repr,我们知道是调用函数long_to_decimal_string,而这个函数实际调用的是long_to_decimal_string_internal

源文件:Objects/longobject.c

static int
long_to_decimal_string_internal(PyObject *aa,
                                PyObject **p_output,
                                _PyUnicodeWriter *writer,
                                _PyBytesWriter *bytes_writer,
                                char **bytes_str)
{
    PyLongObject *scratch, *a;
    PyObject *str = NULL;
    Py_ssize_t size, strlen, size_a, i, j;
    digit *pout, *pin, rem, tenpow;
    int negative;
    int d;
    enum PyUnicode_Kind kind;

    a = (PyLongObject *)aa;
    printf("ob_size     = %d\n"Py_SIZE(a));
    for (int index = 0; index < abs(Py_SIZE(a)); ++index) {
        printf("ob_digit[%d] = %d\n", index, a->ob_digit[index]);
    }
    if (a == NULL || !PyLong_Check(a)) {
        PyErr_BadInternalCall();
        return -1;
    }
    // ......
}

然后我们重新编译源码,这里大概讲一下

make clean
./configure --prefix=<你要安装的路径>
make && make install

然后进入安装路径,bin目录下,即可执行运行重新编译后的*Python*

接下来,我们按照正数、负数、零来找几个案例看看输出结果

print(0) # ob_size     = 0 
print(5) # ob_size     = 1   ob_digit[0] = 5
print(-5) # ob_size     = -1  ob_digit[0] = 5
print(10000000000) # ob_size     = 2  ob_digit[0] = 336323584  ob_digit[1] = 9
print(-10000000000) # ob_size     = -2  ob_digit[0] = 336323584  ob_digit[1] = 9

从输出结果可以看出:

  • 整数0,ob_size=0,ob_digit为空,无需分配

  • 整数5,绝对值保存在ob_digit数组中,数组长度为1,ob_size=1

  • 整数-5,绝对值保存在ob_digit数组中,数组长度为1,ob_size=-1

  • 整数10000000000,ob_size=2,两个数组元素为336323584、9,如何得到10000000000呢?

     336323584 * 2 ** (30 * 0) + 9 * 2 ** (30 * 1) = 10000000000
    

​ 注:这里的 30 是由 PyLong_SHIFT 决定的,64 位系统中,PyLong_SHIFT 为 30,否则 PyLong_SHIFT 为 15

#define PyLong_SHIFT    30
  • 整数-10000000000,ob_size=-2

总结:

  • 整数的绝对值保存在ob_digit数组中
  • ob_digit数组长度保存在ob_size字段,如果是负数,则ob_size结果为负
  • 0的数组为空

整数对象的数值操作

通过类型对象中tp_as_numbertp_as_sequencetp_as_mapping可以看到整数对象支持数值型操作long_as_number

源文件:Objects/longobject.c

static PyNumberMethods long_as_number = {
    (binaryfunc)long_add,       /*nb_add*/
    (binaryfunc)long_sub,       /*nb_subtract*/
    (binaryfunc)long_mul,       /*nb_multiply*/
    long_mod,                   /*nb_remainder*/
    long_divmod,                /*nb_divmod*/
    long_pow,                   /*nb_power*/
    (unaryfunc)long_neg,        /*nb_negative*/
    long_long,                  /*tp_positive*/
    (unaryfunc)long_abs,        /*tp_absolute*/
    (inquiry)long_bool,         /*tp_bool*/
    (unaryfunc)long_invert,     /*nb_invert*/
    long_lshift,                /*nb_lshift*/
    long_rshift,                /*nb_rshift*/
    long_and,                   /*nb_and*/
    long_xor,                   /*nb_xor*/
    long_or,                    /*nb_or*/
    long_long,                  /*nb_int*/
    0,                          /*nb_reserved*/
    long_float,                 /*nb_float*/
    0,                          /* nb_inplace_add */
    0,                          /* nb_inplace_subtract */
    0,                          /* nb_inplace_multiply */
    0,                          /* nb_inplace_remainder */
    0,                          /* nb_inplace_power */
    0,                          /* nb_inplace_lshift */
    0,                          /* nb_inplace_rshift */
    0,                          /* nb_inplace_and */
    0,                          /* nb_inplace_xor */
    0,                          /* nb_inplace_or */
    long_div,                   /* nb_floor_divide */
    long_true_divide,           /* nb_true_divide */
    0,                          /* nb_inplace_floor_divide */
    0,                          /* nb_inplace_true_divide */
    long_long,                  /* nb_index */
};

从源码中可以看出,整数对象支持加(long_add)、减(long_sub)、乘(long_mul)、除(long_divmod)、取模(long_mod)、指数(long_pow)等

static PyObject *
long_add(PyLongObject *a, PyLongObject *b)
{
    CHECK_BINOP(a, b);
    return _PyLong_Add(a, b);
}

可以看到会调用CHECK_BINOP宏检查参数的类型,然后核心逻辑调用_PyLong_Add

PyObject *
_PyLong_Add(PyLongObject *a, PyLongObject *b)
{
    if (IS_MEDIUM_VALUE(a) && IS_MEDIUM_VALUE(b)) {
        return _PyLong_FromSTwoDigits(medium_value(a) + medium_value(b));
    }

    PyLongObject *z;
    if (Py_SIZE(a) < 0) {
        if (Py_SIZE(b) < 0) {
            z = x_add(a, b);
            if (z != NULL) {
                /* x_add received at least one multiple-digit int,
                   and thus z must be a multiple-digit int.
                   That also means z is not an element of
                   small_ints, so negating it in-place is safe. */
                assert(Py_REFCNT(z) == 1);
                Py_SET_SIZE(z, -(Py_SIZE(z)));
            }
        }
        else
            z = x_sub(b, a);
    }
    else {
        if (Py_SIZE(b) < 0)
            z = x_sub(a, b);
        else
            z = x_add(a, b);
    }
    return (PyObject *)z;
}
  • 开始调用IS_MEDIUM_VALUE宏检查a和b是否是中等大小的PyLong对象,如果是直接采用一种优化的方式计算并返回。就是调用_PyLong_FromSTwoDigits得到PyLong对象
  • 如果a为负数、b为负数,调用x_add计算两者绝对值之后,在将结果设置为负
  • 如果a为负数、b为正数,调用x_sub计算两者绝对值之差
  • 如果a为正数、b为负数,调用x_sub计算两者绝对值之差
  • 如果a为正数、b为正数,调用x_add计算两者绝对值之和

可以看到,加法运算实际转化为了绝对值加法x_add、绝对值减法x_add以及转为C整数想加

转为C整数想加

先来看看优化计算方式,怎么判断是否是中等大小的PyLong对象呢?

/* Is this PyLong of size 1, 0 or -1? */
#define IS_MEDIUM_VALUE(x) (((size_t)Py_SIZE(x)) + 1U < 3U)

看注释知道判断PyLong对象的大小是否是1、0或者-1

/* convert a PyLong of size 1, 0 or -1 to a C integer */
static inline stwodigits
medium_value(PyLongObject *x)
{
    assert(IS_MEDIUM_VALUE(x));
    return ((stwodigits)Py_SIZE(x)) * x->ob_digit[0];
}

medium_value是将中等大小的PyLong转为C整数

我们在源码中加入打印,看啥时候会转为C整数

PyObject *
_PyLong_Add(PyLongObject *a, PyLongObject *b)
{
    if (IS_MEDIUM_VALUE(a) && IS_MEDIUM_VALUE(b)) {
        PyObject *str = PyUnicode_FromString("IS_MEDIUM_VALUE: true");
        PyObject_Print(str, stdout, 0);
        printf("\n");
        return _PyLong_FromSTwoDigits(medium_value(a) + medium_value(b));
    }

    PyLongObject *z;
    if (Py_SIZE(a) < 0) {
    // ......
}

重新编译后,我们执行

0 + 5  // IS_MEDIUM_VALUE: true
-5 + (-5// IS_MEDIUM_VALUE: true
2 ** 30 -1 + (2 ** 30 -1// IS_MEDIUM_VALUE: true
2 ** 30 -1 + (2 ** 30// 不会输出IS_MEDIUM_VALUE: true

根据之前打印ob_size我们很容易知道,0的ob_size为0,5的ob_size为1,-5的ob_size为-1,2 ** 30 -1的ob_size为1,2 ** 30的ob_size为2,因此最后一个不满足中等PyLong对象的条件。

这样我们看到ob_size为1的整数范围(-2**30,2**30),开区间

这样设计的好处:可以将性能损耗降低,想想如果是大整数相加,值越大,底层数组越长,运算开销越大。

x_add

大整数相加是如何实现的呢?(数组长度大于1)

static PyLongObject *
x_add(PyLongObject *a, PyLongObject *b)
{
    Py_ssize_t size_a = Py_ABS(Py_SIZE(a)), size_b = Py_ABS(Py_SIZE(b));
    PyLongObject *z;
    Py_ssize_t i;
    digit carry = 0;

    /* Ensure a is the larger of the two: */
    if (size_a < size_b) {
        { PyLongObject *temp = a; a = b; b = temp; }
        { Py_ssize_t size_temp = size_a;
            size_a = size_b;
            size_b = size_temp; }
    }
    z = _PyLong_New(size_a+1);
    if (z == NULL)
        return NULL;
    for (i = 0; i < size_b; ++i) {
        carry += a->ob_digit[i] + b->ob_digit[i];
        z->ob_digit[i] = carry & PyLong_MASK;
        carry >>= PyLong_SHIFT;
    }
    for (; i < size_a; ++i) {
        carry += a->ob_digit[i];
        z->ob_digit[i] = carry & PyLong_MASK;
        carry >>= PyLong_SHIFT;
    }
    z->ob_digit[i] = carry;
    return long_normalize(z);
}

大概解释一下:

首先,计算a、b的数组长度

然后,如果a的数组长度小于b的数组长度,进行交换,使长度大的在前面

然后,创建一个大小为 size_a+1 的新的长整数对象,并将其赋给变量 z,保存最终计算结果

如果对象创建失败(即内存分配失败),则返回空指针表示创建失败

然后,遍历数组长度小的b,carry += a->ob_digit[i] + b->ob_digit[i];:将进位标志 carrya 中的第 i 个数字和 b 中的第 i 个数字相加,结果保存到 carry 中。z->ob_digit[i] = carry & PyLong_MASK;carry 的低 PyLong_SHIFT 位保存到 z 的第 i 个数字中,其中 PyLong_MASK 是一个掩码,用于仅保留低 PyLong_SHIFT 位;carry >>= PyLong_SHIFT;:将 carry 右移 PyLong_SHIFT 位,舍去已经处理过的位,以便下一轮循环使用。

然后,遍历a中剩余的数字

最后,将carry的值保存到z的最高位数字中

返回,标准化z,去除计算结果z底层数组中前面多余的o

画个图来理解

2**30 + 2**30

Python 整数对象_01.png

x_sub
static PyLongObject *
x_sub(PyLongObject *a, PyLongObject *b)
{
    Py_ssize_t size_a = Py_ABS(Py_SIZE(a)), size_b = Py_ABS(Py_SIZE(b));
    PyLongObject *z;
    Py_ssize_t i;
    int sign = 1;
    digit borrow = 0;

    /* Ensure a is the larger of the two: */
    if (size_a < size_b) {
        sign = -1;
        { PyLongObject *temp = a; a = b; b = temp; }
        { Py_ssize_t size_temp = size_a;
            size_a = size_b;
            size_b = size_temp; }
    }
    // ......
}

这部分源码可以自行看看,差不多

到这里我们应该了解了整数的底层逻辑,想必接下来这道题你肯定知道答案了

看一段Java程序

class Demo{

    public static void main(String[] args) {
        int a = 1000000;
        System.out.println(a*a);

    }
}

发现输出结果不是1000000000000,而是-727379968

我们在看看Python程序

1000000 * 1000000 # 1000000000000

发现Python程序执行结果正常,这是为啥呢?

在计算机中,由于变量类型存储空间固定,它能表示的数值范围也是有限的。以 int 为例,该类型长度为 32 位,能表示的整数范围为 [-2**31, 2**31 - 1] 。一万亿显然超出该范围,也就是程序发生了 整数溢出

而Python的int对象是通过整数数组实现的,之前我们输出ob_size,知道大整数的ob_size>1,也就是Python将其拆成若干部分,保存到ob_digit数组中。

想要第一时间看到最新文章,可以关注公众号:郝同学的测开日记