文章目录
- 相关位置文件
- 加法的实现
- 减法的实现
- 乘法的实现
- 内存构造
- 内部元素如何存储
- 整数 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
函数中找到它的实现。
内存构造
在 python3 之后, 就只有一个叫做 int 的类型了, python2.x 的 long 类型在 python3.x 里面就叫做 int 类型
int 在内存空间上的构造和 tuple 元素在内存空间的构造 非常相似
很明显, 只有 ob_digit 这一个位置可以用来存储真正的整数, 但是 cpython 如何按照字节来存储任意长度的整型的呢?
我们来看看
内部元素如何存储
整数 0
注意, 当要表示的整数的值为 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
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
当 i 变成 -1 时候, 唯一的和整数 1 的区别就是储存在 ob_size 里的值变成了 -1, 这里的负号表示这个整数的正负性, 不影响到 ob_digit 里面的值
i = -1
整数 1023
对于 PyLongObject 来说, 最基本的存储单位是 digit, 在我这里是 2个 byte 的大小(16个bit). 1023 只需要占用最右边的10个bit 就够了, 所以 ob_size 里的值仍然是 1
整数 32767
整数 32768
我们发现, cpython 并不会占用掉一个 digit 的所有的 bit 去存储一个数, 第一个 bit 会被保留下来, 我们后面会看到这个保留下来的 bit 有什么作用
小端大端
注意, 因为 digit 作为 cpython 表示整型的最小存储单元, digit 里面的 byte 存储的顺序和你的机器的顺序一致
而 digit 和 digit 之间则是按照 权重最重的 digit 在最右边 原则存储的(小端存储)
我们来看一看整数值 -262143 的存储方式
负号依旧存储在 ob_size 里面
整数值 262143(2^18 = 262144) 的二进制表示应该是 00000011 11111111 11111111
第一个保留位
为什么 digit 的最左边一位需要保留下来呢? 为什么 ob_digit 里面的 digit 的顺序是按照小端的顺序存储的呢?
我们尝试跑一个简单的加法来理解一下
i = 1073741824 - 1 # 1 << 30 == 1073741824
j = 1
k = i + j
首先, 相加之前会初始化一个中间变量我这里叫做 temp, 他的类型也是 PyLongObject, 并且大小为 max(size(i), size(j)) + 1
第一步, 把两个数 ob_digit 里的第一个坑位里的 digit 加起来, 并加到 carry 里
第二步, 把 temp[0] 里的值设置为 (carry & PyLong_MASK)
第三步, 右移 carry, 值保留下最左边的一位(这一位其实就是之前两个数相加的进位)
第四步, 把两边下一个 ob_digit 的对应位置的值加起来, 并把结果与 carry 相加
第五步, 把 temp[1] 里的值设置为 (carry & PyLong_MASK)
第六步, 再次右移
回到步骤四, 直到两边都没有剩余的 digit 可以相加为止, 把最后的 carry 存储到 temp 最后一格
temp 这个变量此时按照 PyLongObject 的方式存储了前面两个数的和, 现在细心地你应该发现了, 第一个保留位是为了用来相加或者相减的时候作进位/退位 用的, 并且当 digit 和 digit 之间按照小端的方式存储的时候, 你做加减法的时候只需要从左到右处理即可
减法的操作和加法类似, 有兴趣的同学可以自己参考源代码
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