长亭百川云 - 文章详情

探究 Python 整数的加减法,感受大整数的运算哲学与魅力

古明地觉的编程教室

29

2024-07-13

楔子

本篇文章来探讨一下整数的加减法是怎么做的,因为整数在底层采用数组进行存储,所以它的加减法就不像浮点数那么简单。

在介绍浮点数的时候说过,对于数值型对象,它的类型一定实现了 tp_as_number 字段,该字段指向了 PyNumberMethods 结构体实例。结构体里面的每个字段都是一个函数指针,对应数值型对象的一个操作。

比如 nb_add 负责加法操作,nb_substract 负责减法操作等等,本篇文章就来探讨一下。首先整数支持的操作非常多,这里我们只探讨加减法。

整数加减法运算原理

整数的加法和减法分别由 long_add 和 long_sub 实现,但运算的核心却不在这两个函数上,它们内部会调用另外两个函数。因为数组保存了整数的绝对值,所以 Python 将整数的运算转成了绝对值运算,而底层有两个函数专门用来做这件事情,分别是 x_add 和 x_sub。

  • x_add(a, b), 计算 a 和 b 的绝对值之和;

  • x_sub(a, b), 计算 a 和 b 的绝对值之差;

所以整数相加就可以这么做,假设两个整数 a 和 b 相加:

  • 如果 a 和 b 均为负数,那么通过 x_add 计算两者的绝对值之和,然后再取相反数;

  • 如果 a 为负数,b 为正数,那么通过 x_sub 计算 b 和 a 的绝对值之差即可;

  • 如果 a 为正数,b 为负数,那么通过 x_sub 计算 a 和 b 的绝对值之差即可;

  • 如果 a 和 b 均为正数,那么通过 x_add 计算 a 和 b 的绝对值之和即可;

而整数相减也是同理,还是整数 a 和 b,两者相减:

  • 如果 a 为正数,b 为负数,那么通过 x_add 计算两者的绝对值之和即可;

  • 如果 a 为负数,b 为正数,那么通过 x_add 计算两者的绝对值之和,然后再取相反数;

  • 如果 a 和 b 均为正数,那么通过 x_sub 计算 a 和 b 的绝对值之差即可;

  • 如果 a 和 b 均为负数,那么通过 x_sub 计算 b 和 a 的绝对值之差即可;

所以相加时,符号相同会调用 x_add、符号不同会调用 x_sub;相减时,符号相同会调用 x_sub、符号不同会调用 x_add。

这就是 Python 的设计,因为绝对值的加减法不用考虑符号的影响,实现更为简单,所以 Python 将整数运算转化成整数的绝对值运算。那么下面我们的重心就在 x_add 和 x_sub 上面了,看看它们是如何对大整数绝对值进行运算的。

虽然大整数运算较为复杂,同时伴随着效率不高,毕竟是基于数组实现的。但底层有一个机制叫做快分支,可以对简单运算的情况提前进行处理,并且命中率也很高。比如在对 a 和 b 计算加减法的时候,底层会先判断数组的长度是否均等于 1,如果是则说明数组中只有一个元素。这样的话,可以直接拿出来转成 C 的整数进行运算,从而将性能损耗降到最低。

只要整数不超过 2 ** 30 - 1,都可以走快分支,显然这可以满足绝大部分场景,因为这个数字已经很大了。至于 x_add 和 x_sub 则属于通用逻辑,而通用也意味着平庸,但如果快分支没有命中,那么就只能走通用逻辑了。

而我们的重点就是要研究 x_add 和 x_sub 的实现,感受大整数运算的魅力。不过在介绍之前,不妨想象一下我们平时将两个整数相加的时候是怎么做的。

从最低位开始进行相加,逢十进一,ob_digit 也是同理。我们可以把数组中的每一个元素看成是一个整体,只不过它不再是逢十进一,而是逢 2**30 进一。

# 数组的每个元素最大能表示 2 ** 30 - 1  
# 把元素整体想象成我们生活中加法的个位、十位、百位...  
# 然后对应的位相加,逢 2 ** 30 进一  
a = [1024, 22]  
b = [342, 18]  
c = [1024 + 342, 22 + 18]  # [1366, 40]  
  
print(  
    a[0] + a[1] * 2 ** 30  
    +  
    b[0] + b[1] * 2 ** 30  
    ==  
    c[0] + c[1] * 2 ** 30  
)  # True  

所以仍旧是对应的位进行相加,和我们生活中的加法并无本质上的区别。只不过生活中的加法,每一位能表示 0~9,逢十进一;而 Python 底层的加法,因为整数使用数组存储,那么每一个位能表示 0 ~ 2**30-1,逢 2**30 进一。

把 1024、342 想象成个位,把 22、18 想象成十位,并且此时不再是逢十进一,而是逢 2**30 进一。

a = [2 ** 30 - 1, 16]  
b = [2 ** 30 - 1, 21]  
# 由于 a[0] + b[0] 达到了 2 ** 30,所以要进个 1  
# 如果是逢十进一,之后该位要减去十  
# 那么逢 2**30 进一,之后显然要减去 2 ** 30  
c = [a[0] + b[0] - 2 ** 30,  
     a[1] + b[1] + 1]  
  
print(  
    a[0] + a[1] * 2 ** 30  
    +  
    b[0] + b[1] * 2 ** 30  
    ==  
    c[0] + c[1] * 2 ** 30  
)  # True

还是不难理解的,就是把数组中每一个对应的元素分别相加,逢 2**30 进 1。

然后是绝对值减法,和绝对值加法一样,也可以类比生活中的减法,从低位到高位分别相减。如果某一位相减的时候发现不够了,那么要向高位借一位。比如 27 - 9,由于 7 比 9 小,因此向 2 借一位变成 17,减去 9,得 8。但 2 被借了一位,所以剩下 1,因此结果为 18。

a = [5, 3]  
b = [6, 1]  
result = []  
  
# 如果计算 a - b,整个过程是怎样的呢?  
# 首先是 a[0] - b[0],由于 a[0] < b[0]  
# 所以要向高位借一位,而一位是 2 ** 30  
result.append(a[0] + 2 ** 30 - b[0])  
# 然后是 a[1] - b[1]  
# 由于 a[1] 被借走了一个位,因此要减 1  
result.append(a[1] - 1 - b[1])  
print(result)  # [1073741823, 1]  
  
# 验证一下  
print(  
    (a[0] + a[1] * 2 ** 30)  
    -  
    (b[0] + b[1] * 2 ** 30)  
)  # 2147483647  
print(  
    result[0] + result[1] * 2 ** 30  
)  # 2147483647

结果没有问题,以上我们就从原理上介绍了大整数的加减法,下面再看一下源码实现。

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2