你所不知道的 有些时候 Python 中乘法比位运算竟然会更快

属先我们开始正文,一个技术群里老哥提出了这样一个问题,为什么在一些情况下,Python 中的简单乘/除法比位运算要慢。

首先秉持着实事求是的精神,我们先来验证一下:

很多人学习python,不知道从何学起。
很多人学习python,掌握了基本语法过后,不知道在哪里寻找案例上手。
很多已经做案例的人,却不知道如何去学习更加高深的知识。
那么针对这三类人,我给大家提供一个好的学习平台,免费领取****,电子书籍,以及课程的源代码!??¤
QQ群:1057034340

你所不知道的 有些时候 Python 中乘法比位运算竟然会更快

我们发现几个很有趣的现象:

  • 在值 x<=2^30 时,乘法比直接位运算要快
  • 在值 x>2^32 时,乘法显著慢于位运算

这个现象很有趣,那么这个现象的 root cause 是什么?实际上这和 Python 底层的实现有关。

简单聊聊

1. PyLongObject 的实现

在 Python 2.x 时期,Python 中将整型分为两类,一类是 long, 一类是 int 。在 Python3 中这两者进行了合并。目前在 Python3 中这两者做了合并,仅剩一个 long。

首先来看看 long 这样一个数据结构底层的实现:

你所不知道的 有些时候 Python 中乘法比位运算竟然会更快

在这里不用关心,PyObject_VAR_HEAD 的含义,我们只需要关心 ob_digit 即可。

在这里,ob_digit 是使用了 C99 中的“柔性数组”来实现任意长度的整数的存储。这里我们可以看一下官方代码中的文档:

你所不知道的 有些时候 Python 中乘法比位运算竟然会更快

简而言之,Python 是将一个十进制数转为 2^(SHIFT) 进制数来进行存储。这里可能不太好了理解。我来举个例子,在我的电脑上,SHIFT 为 30 ,假设现在有整数 1152921506754330628 ,那么将其转为 2^30 进制表示则为: 4*(2^30)^0+2*(2^30)^1+1*(2^30)^2 。那么此时 ob_digit 是一个含有三个元素的数组,其值为 [4,2,1]。

OK,在明白了这样一些基础知识后,我们回过头去看看 Python 中的乘法运算。

2. Python 中的乘法运算

Python 中的乘法运算,分为两部分,其中关于大数的乘法,Python 使用了 Karatsuba 算法1,具体实现如下:

你所不知道的 有些时候 Python 中乘法比位运算竟然会更快

你所不知道的 有些时候 Python 中乘法比位运算竟然会更快

你所不知道的 有些时候 Python 中乘法比位运算竟然会更快

你所不知道的 有些时候 Python 中乘法比位运算竟然会更快

你所不知道的 有些时候 Python 中乘法比位运算竟然会更快

你所不知道的 有些时候 Python 中乘法比位运算竟然会更快

这里不对 Karatsuba 算法1 的实现做单独解释,有兴趣的朋友可以参考文末的 reference 去了解具体的详情。

在普通情况下,普通乘法的时间复杂度为 n^2 (n 为位数),而 K 算法的时间复杂度为 3n^(log3) ≈ 3n^1.585 ,看起来 K 算法的性能要优于普通乘法,那么为什么 Python 不全部使用 K 算法呢?

很简单,K 算法的优势实际上要在当 n 足够大的时候,才会对普通乘法形成优势。同时考虑到内存访问等因素,当 n 不够大时,实际上采用 K 算法的性能将差于直接进行乘法。

所以我们来看看 Python 中乘法的实现:

你所不知道的 有些时候 Python 中乘法比位运算竟然会更快

在这里我们看到,当两个数皆小于 2^30-1 时,Python 将直接使用普通乘法并返回,否则将使用 K 算法进行计算

这个时候,我们来看一下位运算的实现,以右移为例:

你所不知道的 有些时候 Python 中乘法比位运算竟然会更快

你所不知道的 有些时候 Python 中乘法比位运算竟然会更快

在这里我们能看到,在两侧都是小数的情况下,位移动算法将比普通乘法,存在更多的内存分配等操作。这样也会回答了我们文初所提到的一个问题,“为什么一些时候乘法比位运算更快”。

总结本文差不多就到这里了,实际上通过这次分析我们能得到一些很有趣但是也很冷门的知识。实际上我们目前看到这样一个结果,是 Python 对于我们常见且高频的操作所做的一个特定的设计。而这也提醒我们,Python 实际上对于很多操作都存在自己内建的设计哲学,在日常使用的时候,其余语言的经验,可能无法复用。