Python 3的enumerate速度比Python 2慢吗?
Python 3 在执行最简单的循环时,速度比 Python 2 慢了不少,而且这种情况在更新的 Python 3 版本中似乎还在变得更糟。
我在我的 64 位 Windows 电脑上安装了 Python 2.7.6、Python 3.3.3 和 Python 3.4.0,电脑配置是 Intel i7-2700K,主频 3.5 GHz,同时安装了每个版本的 32 位和 64 位版本。在同一个版本的 Python 中,32 位和 64 位之间的执行速度没有明显差别,但不同版本之间的差异就很大了。接下来我会给出一些测试结果,让大家自己看看:
C:\**Python34_64**\python -mtimeit -n 5 -r 2 -s"cnt = 0" "for i in range(10000000): cnt += 1"
5 loops, best of 2: **900 msec** per loop
C:\**Python33_64**\python -mtimeit -n 5 -r 2 -s"cnt = 0" "for i in range(10000000): cnt += 1"
5 loops, best of 2: **820 msec** per loop
C:\**Python27_64**\python -mtimeit -n 5 -r 2 -s"cnt = 0" "for i in range(10000000): cnt += 1"
5 loops, best of 2: **480 msec** per loop
因为 Python 3 的 "range" 和 Python 2 的 "range" 不一样,实际上 Python 3 的 "range" 和 Python 2 的 "xrange" 是一样的,所以我也对这个进行了计时:
C:\**Python27_64**\python -mtimeit -n 5 -r 2 -s"cnt = 0" "for i in **xrange**(10000000): cnt += 1"
5 loops, best of 2: **320 msec** per loop
大家可以很明显地看到,Python 3.3 的速度几乎是 Python 2.7 的两倍慢,而 Python 3.4 又比 Python 3.3 慢了大约 10%。
我的问题是:有没有什么环境选项或者设置可以解决这个问题,还是说只是因为代码效率低下,或者是解释器在 Python 3 版本中做了更多的工作?
答案似乎是,Python 3 默认使用的 "int" 类型是“无限精度”的整数,这在 Python 2.x 中被称为 "long",而且没有选项可以使用 Python 2 中固定长度的 "int"。正是这些可变长度的 "int" 处理起来比较耗时,正如下面的回答和评论所讨论的那样。
可能 Python 3.4 比 Python 3.3 稍慢,是因为为了支持同步而对内存分配进行了更改,这使得内存的分配和释放速度稍微变慢,这很可能是当前版本的 "long" 处理速度变慢的主要原因。
2 个回答
我从这个问题中学到的总结回答,可能对那些和我有同样疑问的人有所帮助:
减慢的原因是,Python 3.x 中的所有整数变量现在都是“无限精度”的,之前在 Python 2.x 中被称为“长整型”的类型,现在是唯一的整数类型,这个决定来自于PEP 237。根据这个文档,之前的“短整型”不再存在(或者只在内部存在)。
旧的“短整型”变量操作运行得比较快,因为它们可以直接使用底层的机器代码操作,并且优化了新“int”对象的分配,因为它们的大小总是相同的。
现在的“长整型”只通过一个在内存中分配的类对象来表示,因为它可能超过某个固定长度的寄存器/内存位置的位深度;由于这些对象的表示可以根据不同的操作而增长或缩小,因此它们的大小是可变的,不能给它们固定的内存分配。
这些“长整型”目前并不使用完整的机器架构字大小,而是保留一个位(通常是符号位)来进行溢出检查,因此“无限精度长整型”目前被分为 32 位/64 位架构的 15 位/30 位切片“数字”。
这些“长整型”的常见用途通常不需要超过一个(或者在 32 位架构中可能需要两个)“数字”,因为一个“数字”的范围大约是 10 亿/32768,分别对应于 64 位/32 位架构。
对于进行一到两个“数字”操作的 'C' 代码来说,效率是相对合理的,因此在许多常见用途中,实际计算的性能损失并没有比简单的“短整型”高多少,尤其是与运行字节码解释器循环所需的时间相比。
最大的性能损失可能是频繁的内存分配和释放,每次循环整数操作都需要一对,这个开销相当大,尤其是当 Python 开始支持多线程和同步锁时(这可能是为什么 Python 3.4 比 3.3 更糟的原因)。
目前的实现总是通过为最大的操作数分配一个额外的“数字”来确保有足够的“数字”,如果有可能“增长”,执行操作(可能会用到那个额外的“数字”),然后将结果长度标准化,以考虑实际使用的“数字”数量,这个数量可能保持不变(或者在某些操作中“缩小”);这通过在“长整型”结构中减少大小计数而不进行新的分配来完成,因此可能浪费一个“数字”的内存空间,但节省了又一次分配/释放周期的性能成本。
有希望提高性能: 对于许多操作,可以预测操作是否会导致“增长”——例如,对于加法,只需查看最高有效位(MSB),如果两个 MSB 都是零,则操作不会增长,这在许多循环/计数操作中都是如此;减法是否“增长”取决于两个操作数的符号和 MSB;左移操作只有在 MSB 为 1 时才会“增长”;等等。
对于像“cnt += 1”或“i += step”这样的情况(为许多用例打开了就地操作的可能性),可以调用一个“就地”版本的操作,这个版本会进行适当的快速检查,只有在需要“增长”时才分配一个新对象,否则就在第一个操作数的位置上进行操作。复杂之处在于编译器需要生成这些“就地”字节码,但这已经完成,生成了适当的特殊“就地操作”字节码,只是当前的字节码解释器将它们指向上述的常规版本,因为这些尚未实现(表中为零/空值)。
可能只需编写这些“就地操作”的版本,并将它们填入“长整型”方法表中,字节码解释器如果存在就会找到并运行它们,或者对表进行小的修改以使其调用这些操作,这样就可以完成。
请注意,浮点数的大小始终相同,因此可以进行相同的改进,尽管浮点数是以空闲位置的块进行分配以提高效率;对于“长整型”来说,做这件事会更困难,因为它们占用的内存量是可变的。
还要注意,这样做会破坏“长整型”(和可选的浮点数)的不可变性,这就是为什么没有定义就地操作符,但它们在这些特殊情况下被视为可变的事实并不会影响外部世界,因为外部世界永远不会意识到有时给定对象与旧值具有相同的地址(只要相等比较是查看内容而不是对象地址)。
我相信,通过避免这些常见用例的内存分配/释放,Python 3.x 的性能将非常接近 Python 2.7。
我在这里学到的很多内容来自于Python 源代码中“长整型”对象的 'C' 文件
编辑补充: 哎呀,忘了说如果变量有时是可变的,那么对局部变量的闭包就不起作用,或者没有重大变化的情况下不起作用,这意味着上述的就地操作会“破坏”闭包。看起来更好的解决方案是让“长整型”像以前的短整型一样,提前进行空闲分配,即使仅在“长整型”大小不变的情况下(这覆盖了大多数情况,比如循环和计数器)。这样做应该意味着代码在典型使用情况下不会比 Python 2 慢太多。
这个差别是因为把 int
类型换成了 long
类型。显然,使用长整型进行运算会比较慢,因为 long
的运算要复杂一些。
如果你强制让 python2 使用长整型,可以把 cnt
设置为 0L
,这样差别就消失了:
$python2 -mtimeit -n5 -r2 -s"cnt=0L" "for i in range(10000000): cnt += 1L"
5 loops, best of 2: 1.1 sec per loop
$python3 -mtimeit -n5 -r2 -s"cnt=0" "for i in range(10000000): cnt += 1"
5 loops, best of 2: 686 msec per loop
$python2 -mtimeit -n5 -r2 -s"cnt=0L" "for i in xrange(10000000): cnt += 1L"
5 loops, best of 2: 714 msec per loop
在我的电脑上,python3.4 比 python2 使用 range
和 xrange
时都要快,尤其是在使用 long
的情况下。最后一次用 python2 的 xrange
进行的测试显示,这种情况下的差别非常小。
我没有安装 python3.3,所以无法比较 3.3 和 3.4 的表现,但据我所知,这两个版本之间在 range
的方面没有什么重大变化,所以运行时间应该差不多。如果你发现有明显的差别,可以试着用 dis
模块查看生成的字节码。关于内存分配器有个变化(PEP 445),但我不清楚默认的内存分配器有没有被修改,以及这对性能有什么影响。