Bab*_*bak 3 python performance
Python 3.12 两天前发布,包含多项新功能和改进。它声称它比以往任何时候都快,所以我决定尝试一下。我用新版本运行了一些脚本,但速度比以前慢。我尝试了各种方法,每次都简化我的代码,试图找出导致其运行缓慢的瓶颈。然而,到目前为止我还没有成功。最后,我决定测试一个简单的 for 循环,如下所示:
import time
def calc():
for i in range(100_000_000):
x = i * 2
t = time.time()
calc()
print(time.time() - t)
Run Code Online (Sandbox Code Playgroud)
在我的机器上,Python 3.11.5 上需要 4.7 秒,Python 3.12.0 上需要 5.7 秒。在其他机器上尝试也得到了类似的结果。
那么为什么在最新版本的Python中它会变慢呢?
我能够使用 Intel i5-9600KF CPU 在 Debian Linux 6.1.0-6 上重现 CPython 3.11.2 和 CPython 3.12.0rc2 之间观察到的行为。我尝试使用低级分析方法来找出差异。简而言之:您的基准测试非常具体,而 CPython 3.12 对于这种具体情况的优化较少。CPython 3.12 似乎管理对象分配,更具体地说range有点不同。与 CPython 3.11 不同,CPython 3.12 似乎为循环的每次迭代从常量 2 创建一个新对象。此外,主求值函数执行间接函数指针调用,在这种情况下速度特别慢。无论如何,您不应该在这样的用例中使用 (C)Python(CPython 文档中有说明)。
这是我得到的结果(多次启动之间相当稳定):
\n3.11: 2.026395082473755\n3.12: 2.4122846126556396\nRun Code Online (Sandbox Code Playgroud)\n因此,在我的机器上,CPython 3.12 比 CPython 3.11 慢大约 20%。
\n分析结果表明,一半的开销来自 CPython 3.12 主评估函数中的间接函数指针调用,而这在 CPython 3.11 中是不存在的。这个函数调用在大多数现代处理器上都很昂贵。下面是热点部分的汇编代码:
\n3.11: 2.026395082473755\n3.12: 2.4122846126556396\nRun Code Online (Sandbox Code Playgroud)\n虽然 CPython 3.11 中同一函数的汇编代码类似,但没有如此昂贵的调用。尽管如此,CPython 3.11 中已经存在许多类似的间接函数调用。我的假设是,这样的调用在 CPython 3.12 中成本更高,因为硬件预测单元不太可预测它(可能是因为同一条指令调用多个函数)。有关这方面的更多信息,请阅读这篇精彩的文章。关于这部分我不能说太多,因为汇编代码确实很大(而且事实证明 C 代码也很大)。
\n其余的开销似乎来自 CPython 3.12 中管理对象(更具体地说是常量)的方式。事实上,在 CPython 3.11 中,PyObject_Free调用很慢(因为您花费了所有时间创建/删除对象),而在 CPython 3.12 中,这样的调用甚至在分析器中不可见,但有一个相当PyLong_FromLong慢的调用(在 CPython 3.12 中不可见) CPython 3.11)。其余(许多其他)功能仅占用不到 25~30%,并且在两个版本中看起来相似。基于此,我们可以得出结论,与CPython 3.11 不同,CPython 3.12 会为循环的每次迭代根据常量创建一个新对象。2在这种情况下,这显然效率不高(应该记住,CPython 是一种解释器,而不是编译器,因此它对此不执行优化也就不足为奇了)。有一种简单的方法可以检查:在循环之前存储2在变量中并在循环中使用该变量。这是更正后的代码:
\xe2\x94\x82 \xe2\x86\x93 je 4b8\n 1,28 \xe2\x94\x82 mov (%rdi),%rax\n 0,33 \xe2\x94\x82 test %eax,%eax\n 0,61 \xe2\x94\x82 \xe2\x86\x93 js 4b8\n 0,02 \xe2\x94\x82 sub $0x1,%rax\n 2,80 \xe2\x94\x82 mov %rax,(%rdi)\n \xe2\x94\x82 \xe2\x86\x93 jne 4b8\n 0,08 \xe2\x94\x82 mov 0x8(%rdi),%rax\n 16,28 \xe2\x94\x82 \xe2\x86\x92 call *0x30(%rax) <---------------- HERE\n \xe2\x94\x82 nop\n 1,53 \xe2\x94\x82 4b8: mov (%rsp),%rax\n \xe2\x94\x82 lea 0x4(%rax),%rcx\n \xe2\x94\x82 movzbl 0x3(%rax),%eax\n 0,06 \xe2\x94\x82 mov 0x48(%r13,%rax,8),%rdx\n 1,82 \xe2\x94\x82 mov (%rdx),%eax\n 0,04 \xe2\x94\x82 add $0x1,%eax\n \xe2\x94\x82 \xe2\x86\x93 je 8800\nRun Code Online (Sandbox Code Playgroud)\n以下是更正代码的时间:
\nimport time\n\ndef calc():\n const = 2\n for i in range(100_000_000):\n x = i * const\n\nt = time.time()\ncalc()\nprint(time.time() - t)\nRun Code Online (Sandbox Code Playgroud)\nCPython 3.12 版本现在比以前快得多,而其他版本几乎不受代码修改的影响。乍一看,它倾向于证实最后一个假设。话虽这么说,探查器仍然报告PyLong_FromLong修改后的代码中的许多调用!事实证明,这一更改消除了本节开头讨论的与间接函数指针调用相关的问题!
我的假设是,PyLong_FromLong调用来自不同的方式来管理从range(ie i) 生成的对象。以下代码倾向于确认这一点(请注意,由于列表的原因,该代码需要约 4 GiB 的 RAM,因此不应在生产中使用,而只能用于测试目的):
3.11: 2.045902967453003\n3.12: 2.2230796813964844\nRun Code Online (Sandbox Code Playgroud)\n以下是我机器上的结果:
\nimport time\n\ndef calc():\n const = 2\n TMP = list(range(100_000_000))\n t = time.time()\n for i in TMP:\n x = i * const\n print(time.time() - t)\n\ncalc()\nRun Code Online (Sandbox Code Playgroud)\n间隙比以前更近,循环的时间也更短,因为对象都是之前在列表中预先计算的。分析结果确认PyLong_FromLong不会在定时循环中调用。因此,range在这种情况下,CPython 3.12 的速度较慢。
其余的开销很小(<4%)。这种性能差距可能来自编译器优化,甚至是 CPython 源代码中非常微小的更改。例如,条件跳转地址等简单的事情可能会显着影响许多 Intel CPU 的性能结果(请参阅:JCC 勘误表)。像这样的小细节很重要,而且编译器并不完美。这就是为什么这种性能变化很常见并且是预料之中的,因此不值得调查。
\n顺便说一句,如果您关心性能,那么请使用Cython 或 PyPy来执行此类计算代码。
\n| 归档时间: |
|
| 查看次数: |
2107 次 |
| 最近记录: |