【问题标题】:Is there a reason Python 3 enumerates slower than Python 2?Python 3 的枚举速度比 Python 2 慢吗?
【发布时间】:2014-06-20 14:43:06
【问题描述】:

对于最小循环的枚举,Python 3 似乎比 Python 2 慢很多,随着 Python 3 的更新版本,这种情况似乎变得更糟。

我在我的 64 位 Windows 机器(Intel i7-2700K - 3.5 GHz)上安装了 Python 2.7.6、Python 3.3.3 和 Python 3.4.0,每个机器都有 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 的“范围”与 Python 2 的“范围”不同,并且在功能上与 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

可以很容易地看出,3.3 版的速度几乎是 2.7 版的两倍,而 Python 3.4 又比 2.7 慢了大约 10%。

我的问题:是否有解决此问题的环境选项或设置,还是只是低效的代码或解释器为 Python 3 版本做的更多?


答案似乎是 Python 3 使用了在 Python 2.x 中被称为“long”的“无限精度”整数,它的默认“int”类型没有任何使用 Python 2 固定位长度的选项” int”,并且处理这些可变长度的“int”会花费额外的时间,如下面的答案和 cmets 中所述。

Python 3.4 可能比 Python 3.3 慢一些,因为为了支持同步而对内存分配进行了更改,导致内存分配/释放速度稍慢,这可能是当前版本的“长”处理运行速度较慢的主要原因。

【问题讨论】:

  • 至少因为在 Python 3 中,整数的默认类型是 long long
  • 我同意frostnational。尝试在 python2 基准测试中将cnt = 0 更改为cnt = 0L,看看这是否会有所不同。我刚刚在我的机器上尝试过,python3 比 python2 的 range 快两倍,比 python 2 xrange 快 30%(至少在我的盒子上)。
  • @frostnational,不,比“long long”或 64 位但“无限精度”整数更差,它具有表示给定值所需的字节数/字数。如果可能的话,除了等待 Python 优化实现之外,似乎没有其他方法可以恢复速度。

标签: python performance loops python-2.7 python-3.x


【解决方案1】:

差异是由于int 类型替换为long 类型所致。显然,长整数的操作会更慢,因为long 操作更复杂。

如果您通过将cnt 设置为0L 来强制python2 使用长整数,那么差异就会消失:

$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 比使用range 和使用longs 时使用xrange 的python2 都快。使用 python 的 2 xrange 进行的最后一个基准测试表明,这种情况下的差异很小。

我没有安装 python3.3,所以我无法在 3.3 和 3.4 之间进行比较,但据我所知,这两个版本之间没有显着变化(关于range),所以时间应该是大约相同。如果您发现有显着差异,请尝试使用 dis 模块检查生成的字节码。内存分配器 (PEP 445) 发生了变化,但我不知道默认内存分配器是否被修改以及性能方面的后果。

【讨论】:

  • 您已经解释了为什么 Python 3 使用 long 会更慢,谢谢,并且您已经展示了如何使 Python 2 在我的机器上几乎与 Python 3.3 一样慢,而不像 3.4 一样慢(如果可以将 xrange 也强制为 long,它可能会更慢);但是,当不需要长的范围时,您还没有给我一种使 Python 3 与 Python 2 一样快的方法。我猜没有任何办法,因为 Python 已经丢弃了 32 位整数。这似乎是一个疏忽,因为不关心性能?对于简单的计数,有内置方法,但不适用于迭代器/生成器
  • @GordonBGood:好问题。无论如何,性能可能不是 Python 开发最重要的目标。总会有取舍。可以想象,Python 3 以后会加入优化。
  • @pepr,Bakuriu 让我思考和阅读,我现在看到 Python 3 不仅使用 int 'C' 类型“long long”'s = 64 位,而是同时使用 Python 2“ long"'s 和 Python 3 "int"(与 Python 2 "long" 相同)是无限精度整数,具有表示值所需的位数/字节数。变化是删除了通常的固定长度整数,只支持这些无限精度整数。它很好地解释了为什么枚举 Python 3 的范围如此缓慢,并且使用的值越大,速度就越慢。是的,这些应该能够针对像这样的小范围进行优化。
  • 了解内存使用情况,这些“长”整数仅在需要额外精度时才使用更多内存;我主要担心的是性能下降。考虑到在 F# 中对循环索引和计数变量使用无限精度 BigInt 的等效循环需要大约 2.3 秒或慢三倍以下,它已经得到了很好的优化。我也看到了消除额外类型的意义,但遗憾的是执行速度几乎是三倍的成本(至少在我的机器上):PEP 237
  • @WinEunuuchs2Unix Ubuntu 16.04 旧,python3.5 也旧,python 2.7.12 也旧。在我的带有 python 2.7.18 和 python3.8.5 的 Ubuntu 20.04 机器上,我看到 python3 在所有情况下都比 python2 快:python2 -mtimeit -n5 -r2 -s"cnt=0" "for i in range(10000000): cnt += 1" 5 loops, best of 2: 300 msec per loop 而 python3:python3 -mtimeit -n5 -r2 -s"cnt=0" "for i in range(10000000): cnt += 1" 5 loops, best of 2: 285 msec per loop 也许差异可以归因于 32 位和 64 位?我相信在 2014 年我使用的是 32 位笔记本电脑,现在我有了更好的 64 位台式机。
【解决方案2】:

我从这个问题中学到的总结答案可能对其他和我一样想知道同样事情的人有所帮助:

  1. 速度变慢的原因是 Python 3.x 中的所有整数变量现在都是“无限精度”的类型,以前在 Python 2.x 中被称为“long”,但现在是唯一的整数类型由PEP 237 决定。根据该文档,具有基本架构位深度的“短”整数不再存在(或仅在内部存在)。

  2. 旧的“短”变量操作可以相当快地运行,因为它们可以直接使用底层机器代码操作并优化新“int”对象的分配,因为它们始终具有相同的大小。

  3. “long”类型目前仅由分配在内存中的类对象表示,因为它可能超过给定的固定长度寄存器/内存位置的位深度;由于这些对象表示可能会因各种操作而增长或缩小,因此具有可变大小,因此不能为它们分配固定的内存分配并留在那里。

  4. 这些“长”类型(当前)不使用完整的机器架构字长,而是保留一个位(通常是符号位)来进行溢出检查,因此“无限精度长”被划分(当前) 分别转换为 32 位/64 位架构的 15 位/30 位切片“数字”。

  5. 这些“长”整数的许多常见用途不需要超过一个(或者对于 32 位架构可能是两个)“数字”,因为一个“数字”的范围约为十亿/ 32768 分别用于 64 位/32 位架构。

  6. 1234563与运行字节码解释器循环所需的时间相比,随着实际计算的进行。
  7. 最大的性能损失可能是常量内存分配/释放,每个循环整数操作一对非常昂贵,尤其是当 Python 开始支持多线程时带同步锁(这可能是 Python 3.4 比 3.3 差的原因)。

  8. 目前,如果有可能“增长”,则实现始终通过在用于最大操作数的“数字”的实际大小之上分配一个额外的“数字”来确保足够的“数字”,操作(可能会或可能不会实际使用那个额外的“数字”),然后规范化结果长度以考虑实际使用的“数字”数量,这可能实际上保持不变(或可能“缩小”某些操作) ;这是通过在没有新分配的情况下仅减少“长”结构中的大小计数来完成的,因此可能会浪费一个“数字”的内存空间,但会节省另一个分配/释放周期的性能成本。

  9. 有希望提高性能: 对于许多操作,可以预测该操作是否会导致“增长” - 例如,对于一个加法,只需查看最高有效位 (MSB) 和操作如果两个 MSB 都为零则无法增长,这将是许多循环/计数器操作的情况;根据两个操作数的符号和 MSB,减法不会“增长”;仅当 MSB 为 1 时,左移才会“增长”;等等。

  10. 对于那些语句类似于“cnt += 1”/“i += step”等的情况(为许多用例打开了就地操作的可能性),一个“就地”可以调用操作的版本,该版本将进行适当的快速检查,并且仅在需要“增长”时才分配新对象,否则代替第一个操作数进行操作。复杂之处在于编译器需要生成这些“就地”字节码,然而,这已经完成,并产生了适当的特殊“就地操作”字节码,只是这样当前的字节码解释器将它们定向到如上所述的通常版本,因为它们尚未实现(表中的零值/空值)。

  11. 很可能所有要做的就是编写这些“就地操作”的版本,并将它们填充到“长”方法表中,字节码解释器已经找到并运行它们,如果它们存在或对表进行微小更改以使其调用它们是所需的全部。

请注意,浮点数总是相同的大小,因此可以进行相同的改进,尽管浮点数被分配在备用位置块中以提高效率;对于“长”来说,这样做会困难得多,因为它们占用的内存量是可变的。

还请注意,这会破坏“long”(以及可选的 float)的不变性,这就是为什么没有定义就地运算符的原因,但仅在这些特殊情况下它们被视为可变的事实并没有影响外部世界,因为它永远不会意识到有时给定对象与旧值具有相同的地址(只要相等比较查看内容而不仅仅是对象地址)。

我相信通过避免这些常见用例的内存分配/释放,Python 3.x 的性能将非常接近 Python 2.7。

我在这里学到的大部分内容来自Python trunk 'C' source file for the "long" object


EDIT_ADD: 哎呀,忘了如果变量有时是可变的,那么局部变量上的闭包不起作用或在没有重大更改的情况下不起作用,这意味着上述就地操作会“中断”关闭。似乎更好的解决方案是让提前备用分配快速地为“长”工作,就像它过去用于短整数和浮点一样,即使只是在“长”大小不适用的情况下更改(根据问题涵盖大部分时间,例如 for 循环和计数器)。这样做应该意味着代码在典型应用中的运行速度不会比 Python 2 慢很多。

【讨论】:

    猜你喜欢
    • 2017-11-05
    • 2012-12-25
    • 1970-01-01
    • 2016-02-07
    • 2011-06-06
    • 1970-01-01
    • 2015-10-11
    • 2015-08-08
    • 1970-01-01
    相关资源
    最近更新 更多