文章目录

  • 相关位置文件
  • 加法的实现
  • 减法的实现
  • 乘法的实现
  • 内存构造
  • 内部元素如何存储
  • 整数 0
  • 整数 1
  • 整数 -1
  • 整数 1023
  • 整数 32767
  • 整数 32768
  • 小端大端
  • 第一个保留位
  • small ints


相关位置文件

  • cpython/Objects/longobject.c
  • cpython/Include/longobject.h
  • cpython/Include/longintrepr.h

加法的实现

整数是“数字方式”持久化的,这意味着加法就像我们在小学学到的一样简单,python 的源代码向我们展示了这也是它的实现方式。文件longobject.c中名为x_add的函数执行两个数字的相加。

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;

上面的代码片段取自x_add函数,您可以看到它遍历数字并执行数字加法并计算和传播进位。

当加法的结果是负数时,事情就变得有趣了。的符号ob_size是整数的符号,这意味着,如果你有一个负数,那么它就是ob_size负数。的绝对值ob_size将决定 中的位数ob_digit。

减法的实现

与加法的实现方式类似,减法也以数字方式进行。文件longobject.c中名为x_sub的函数执行两个数字的减法。

for (i = 0; i < size_b; ++i) {
        borrow = a->ob_digit[i] - b->ob_digit[i] - borrow;
        z->ob_digit[i] = borrow & PyLong_MASK;
        borrow >>= PyLong_SHIFT;
        borrow &= 1; /* Keep only one sign bit */
    }
    for (; i < size_a; ++i) {
        borrow = a->ob_digit[i] - borrow;
        z->ob_digit[i] = borrow & PyLong_MASK;
        borrow >>= PyLong_SHIFT;
        borrow &= 1; /* Keep only one sign bit */
    }

上面的代码片段取自x_sub函数,您可以看到它如何迭代数字并执行减法以及计算和传播 burrow。确实非常类似于加法。

乘法的实现

再一次,实现乘法的天真方法将是我们在小学数学中学到的,但它不会很有效。Python,为了保持高效,实现了Karatsuba 算法,该算法在 O ( nˡᵒᵍ³ ) 基本步骤中将两个 n 位数字相乘。
该算法略显复杂,不在本文讨论范围内,但您可以在 longobject.c 文件中的k_mul和k_loplateral_mul

函数中找到它的实现。

内存构造

Python中从两个数中选取较大的数 python求两个整数中的较大数_Python中从两个数中选取较大的数

在 python3 之后, 就只有一个叫做 int 的类型了, python2.x 的 long 类型在 python3.x 里面就叫做 int 类型

int 在内存空间上的构造和 tuple 元素在内存空间的构造 非常相似

很明显, 只有 ob_digit 这一个位置可以用来存储真正的整数, 但是 cpython 如何按照字节来存储任意长度的整型的呢?

我们来看看

内部元素如何存储

整数 0

Python中从两个数中选取较大的数 python求两个整数中的较大数_开发语言_02

注意, 当要表示的整数的值为 0 时, ob_digit 这个数组为空, 不存储任何东西, ob_size 中的 0 就直接表示这个整数的值为 0, 这是一种特殊情况

i = 0

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FYT192S4-1656252681290)(https://github.com/zpoint/CPython-Internals/blob/master/BasicObject/long/0.png)]

整数 1

Python中从两个数中选取较大的数 python求两个整数中的较大数_#define_03

ob_digit 可以有两种不同的定义, 具体是 uint32_t 还是 unsigned short 取决于操作系统

#if PYLONG_BITS_IN_DIGIT == 30
typedef uint32_t digit;
typedef int32_t sdigit;
typedef uint64_t twodigits;
typedef int64_t stwodigits; /* signed variant of twodigits */
#define PyLong_SHIFT    30
#define _PyLong_DECIMAL_SHIFT   9 /* max(e such that 10**e fits in a digit) */
#define _PyLong_DECIMAL_BASE    ((digit)1000000000) /* 10 ** DECIMAL_SHIFT */
#elif PYLONG_BITS_IN_DIGIT == 15
typedef unsigned short digit;
typedef short sdigit; /* signed variant of digit */
typedef unsigned long twodigits;
typedef long stwodigits; /* signed variant of twodigits */
#define PyLong_SHIFT    15
#define _PyLong_DECIMAL_SHIFT   4 /* max(e such that 10**e fits in a digit) */
#define _PyLong_DECIMAL_BASE    ((digit)10000) /* 10 ** DECIMAL_SHIFT */

我把源代码里的 PYLONG_BITS_IN_DIGIT 的值写死成了 15, 这样我下面所有的示例都是以 unsigned short 定义的 digit

当我们需要表示整数 1 的时候, ob_size 的值变成了1, 这个时候 ob_size 表示 ob_digit 的长度, 并且 ob_digit 里以 unsigned short 的表示方式存储了整数1

i = 1

整数 -1

Python中从两个数中选取较大的数 python求两个整数中的较大数_开发语言_04

当 i 变成 -1 时候, 唯一的和整数 1 的区别就是储存在 ob_size 里的值变成了 -1, 这里的负号表示这个整数的正负性, 不影响到 ob_digit 里面的值

i = -1

整数 1023

Python中从两个数中选取较大的数 python求两个整数中的较大数_git_05

对于 PyLongObject 来说, 最基本的存储单位是 digit, 在我这里是 2个 byte 的大小(16个bit). 1023 只需要占用最右边的10个bit 就够了, 所以 ob_size 里的值仍然是 1

整数 32767

Python中从两个数中选取较大的数 python求两个整数中的较大数_git_06

整数 32768

Python中从两个数中选取较大的数 python求两个整数中的较大数_Python中从两个数中选取较大的数_07

我们发现, cpython 并不会占用掉一个 digit 的所有的 bit 去存储一个数, 第一个 bit 会被保留下来, 我们后面会看到这个保留下来的 bit 有什么作用

小端大端

注意, 因为 digit 作为 cpython 表示整型的最小存储单元, digit 里面的 byte 存储的顺序和你的机器的顺序一致

digitdigit 之间则是按照 权重最重的 digit 在最右边 原则存储的(小端存储)

我们来看一看整数值 -262143 的存储方式

负号依旧存储在 ob_size 里面

整数值 262143(2^18 = 262144) 的二进制表示应该是 00000011 11111111 11111111

Python中从两个数中选取较大的数 python求两个整数中的较大数_git_08

第一个保留位

为什么 digit 的最左边一位需要保留下来呢? 为什么 ob_digit 里面的 digit 的顺序是按照小端的顺序存储的呢?

我们尝试跑一个简单的加法来理解一下

i = 1073741824 - 1 # 1 << 30 == 1073741824
j = 1

Python中从两个数中选取较大的数 python求两个整数中的较大数_开发语言_09

k = i + j

首先, 相加之前会初始化一个中间变量我这里叫做 temp, 他的类型也是 PyLongObject, 并且大小为 max(size(i), size(j)) + 1

Python中从两个数中选取较大的数 python求两个整数中的较大数_python_10

第一步, 把两个数 ob_digit 里的第一个坑位里的 digit 加起来, 并加到 carry

Python中从两个数中选取较大的数 python求两个整数中的较大数_git_11

第二步, 把 temp[0] 里的值设置为 (carry & PyLong_MASK)

Python中从两个数中选取较大的数 python求两个整数中的较大数_git_12

第三步, 右移 carry, 值保留下最左边的一位(这一位其实就是之前两个数相加的进位)

Python中从两个数中选取较大的数 python求两个整数中的较大数_git_13

第四步, 把两边下一个 ob_digit 的对应位置的值加起来, 并把结果与 carry 相加

Python中从两个数中选取较大的数 python求两个整数中的较大数_Python中从两个数中选取较大的数_14

第五步, 把 temp[1] 里的值设置为 (carry & PyLong_MASK)

Python中从两个数中选取较大的数 python求两个整数中的较大数_开发语言_15

第六步, 再次右移

Python中从两个数中选取较大的数 python求两个整数中的较大数_git_16

回到步骤四, 直到两边都没有剩余的 digit 可以相加为止, 把最后的 carry 存储到 temp 最后一格

Python中从两个数中选取较大的数 python求两个整数中的较大数_python_17

temp 这个变量此时按照 PyLongObject 的方式存储了前面两个数的和, 现在细心地你应该发现了, 第一个保留位是为了用来相加或者相减的时候作进位/退位 用的, 并且当 digitdigit 之间按照小端的方式存储的时候, 你做加减法的时候只需要从左到右处理即可

减法的操作和加法类似, 有兴趣的同学可以自己参考源代码

Python中从两个数中选取较大的数 python求两个整数中的较大数_#define_18

small ints

cpython 同时也使用了一个全局变量叫做 small_ints 来单例化一部分常见范围的整数, 这么做可以减少频繁的向操作系统申请和释放的次数, 并提高性能

#define NSMALLPOSINTS           257
#define NSMALLNEGINTS           5
static PyLongObject small_ints[NSMALLNEGINTS + NSMALLPOSINTS];

我们来看看

c = 0
d = 0
e = 0
print(id(c), id(d), id(e)) # 4480940400 4480940400 4480940400
a = -5
b = -5
print(id(a), id(b)) # 4480940240 4480940240
f = 1313131313131313
g = 1313131313131313
print(id(f), id(g)) # 4484604176 4484604016

Python中从两个数中选取较大的数 python求两个整数中的较大数_开发语言_19