你能否从CPython的源码分析整数运算的溢出检查是如何做的

访客 源码剖析 1

本文目录导读:

  1. 核心数据结构:PyLongObject
  2. 运算中的 "溢出检查" 本质:进位传播 + 内存分配
  3. 乘法中的特殊处理:Karatsuba 和更大规模检查
  4. 整数转字符串时的溢出(这是最常见报错)
  5. 特例:空循环和优化
  6. 完整路径图(加法为例)
  7. 最终回答用一句话总结

这是一个非常底层的技术问题,CPython 的整数实现非常独特:Python 的整数是任意精度的(大整数),因此在底层操作中,CPython 不依赖于 CPU 硬件的整数溢出标志位,而是通过显式的内存检查和进位传播来防止溢出。

本质上,CPython 的整数运算“没有传统意义上的溢出”,因为它会自动扩展内存来容纳更大的数字,但为了防止分配过大的内存导致崩溃(2 ** (2 ** 100)),它会在分配内存时做显式的大小检查并抛出 OverflowError

下面从源码角度详细拆解,以 Python 3.11 为基准。

核心数据结构:PyLongObject

先看整数对象在底层的表示,在 Include/longobject.hObjects/longobject.c 中:

// digit 通常是一个 30-bit 或 15-bit 的 "小单元"(取决于平台)
typedef uint32_t digit;
struct _longobject {
    PyObject_HEAD
    Py_ssize_t ob_size; // 符号和长度:正数为正,负数为负,绝对值是 digit 数组长度
    digit ob_digit[1];  // 柔性数组,实际长度由 ob_size 决定
};

关键点:

  • ob_size 的绝对值代表有多少个 30-bit 的 "digit"。
  • ob_size 为 0,表示整数 0。
  • 运算发生在这些 digit 数组上,类似于手算多位十进制数。

运算中的 "溢出检查" 本质:进位传播 + 内存分配

当两个大整数相加时,CPython 会预先计算最大可能需要的 digit 个数,然后分配对应的内存,如果这个 digit 个数超过了某个内部限制(2^63 量级),就会直接抛出 OverflowError

让我们看一个最基础的函数 x_add(在 Objects/longobject.c 中):

static PyLongObject *
x_add(PyLongObject *a, PyLongObject *b)
{
    Py_ssize_t size_a = Py_ABS(Py_SIZE(a));
    Py_ssize_t size_b = Py_ABS(Py_SIZE(b));
    PyLongObject *z;
    Py_ssize_t i;
    digit carry = 0;
    // 1. 预先计算结果的 digit 个数:最大 size 加 1
    Py_ssize_t size_z = MAX(size_a, size_b) + 1;
    // 2. 分配内存 —— 这是“溢出检查”的关键
    z = _PyLong_New(size_z); // size_z 过大,会在内部检查并抛出 OverflowError
    if (z == NULL)
        return NULL;
    // 3. 逐 digit 相加,处理进位
    for (i = 0; i < size_z; i++) {
        carry += i < size_a ? a->ob_digit[i] : 0;
        carry += i < size_b ? b->ob_digit[i] : 0;
        z->ob_digit[i] = carry & PyLong_MASK;
        carry >>= PyLong_SHIFT;
    }
    // 4. 去除最高位可能多余的 0(如果最高位没有进位)
    return long_normalize(z);
}

真正的溢出检查发生在 _PyLong_New 中:

PyLongObject *
_PyLong_New(Py_ssize_t size)
{
    PyLongObject *result;
    // 关键检查:size 超过 PyLong_MAX_BYTES 或 Py_ssize_t 最大范围
    // 这里 size 是 digit 个数,每个 digit 通常占4字节
    // 因此检查 size * sizeof(digit) 不会超过内存限制
    if (size > (PY_SSIZE_T_MAX - PyObject_HEAD_SIZE) / sizeof(digit)) {
        // 如果超出,会设置 OverflowError
        PyErr_SetString(PyExc_OverflowError, "Python int too large ...");
        return NULL;
    }
    // 否则分配内存
    result = PyObject_NewVar(PyLongObject, &PyLong_Type, size);
    ...
}

当 digit 个数大到逼近 PY_SSIZE_T_MAX 时,_PyLong_New 会报 OverflowError,这是 Python 整数运算中唯一真正的“溢出”。

乘法中的特殊处理:Karatsuba 和更大规模检查

乘法更复杂,CPython 对小数使用教科书乘法,对大数使用 Karatsuba 乘法甚至 FFT 乘法。

long_mul 函数中,第一步也是预估结果大小:

static PyObject *
long_mul(PyLongObject *v, PyLongObject *w)
{
    // 直观的方法:结果 digit 数 ≈ a_size + b_size
    // 先分配
    z = _PyLong_New(size_a + size_b);
    ...
    // 如果乘法的中间结果导致 digit 个数预估错误,会在分配时捕获
}

整数转字符串时的溢出(这是最常见报错)

你可能见过 OverflowError: int too large to convert to floatOverflowError: cannot fit 'int' into an index-sized integer

这些通常在类型转换时发生,而不是在算术运算时。

>>> 2 ** 20000
# 成功,生成一个长整数
>>> float(2 ** 20000)
# 报错:OverflowError: int too large to convert to float

这是因为 float 只有 53 bit 精度,CPython 在 PyLong_AsDouble 中显式检查:

double
PyLong_AsDouble(PyObject *v)
{
    ...
    // exp > 1024,直接报错
    if (exp > 1024) {
        set_error("int too large to convert to float");
        return -1.0;
    }
    ...
}

特例:空循环和优化

有人会问:while True: x += 1 为什么不会溢出?

因为 Python 整数对象在每次增加时,如果当前 digit 数组能容纳(没有进位导致需要新 digit),就直接修改 ob_digit[0],不需要重新分配,只有当需要新的 digit 时(比如从 2^30-1 到 2^30),才触发新内存分配和相应的 OverflowError 检查。

完整路径图(加法为例)

用户代码:a + b
    → 解释器调用 PyLong_Type 的 nb_add 槽位
    → long_add()
        → x_add() 或 z_add() (取决于符号)
            → 计算 size_z = max(size_a, size_b) + 1
            → _PyLong_New(size_z)
                → if (size > limit) 抛出 OverflowError
                → 分配变量对象
            → 逐 digit 相加,传播进位
            → long_normalize(z) 去除高位零

最终回答用一句话总结

CPython 不依赖 CPU 硬件溢出标志,而是通过预估结果需要的“digit”个数,在内存分配阶段检查是否超出 PY_SSIZE_T_MAX 来防止真正的内存溢出;任何超出该范围的尝试都会抛出 OverflowError

标签: CPython 整数溢出检查 大整数转换

抱歉,评论功能暂时关闭!