我最近正在学习一些 HPC 主题,并且了解到现代 C/C++ 编译器能够检测有权进行优化的地方,并使用相应的技术(如 SIMD、循环展开等)进行优化,尤其是在标志下-O3
,在运行时性能与编译时间和目标文件大小之间进行权衡。
然后我立即想到 CPython 是即时解释和执行的,所以我认为它无法承担这些编译器功能,因为它的编译时间相当于运行时,所以我做了下面的一个玩具实验:
import time, random
n = 512
A = [[random.random() for _ in range(n)] for _ in range(n)]
B = [[random.random() for _ in range(n)] for _ in range(n)]
C = [[0] * n for _ in range(n)]
def matMul( A, B, C ):
""" C = A .* B """
for i in range(0, n - 4, 4):
for j in range(0, n - 4, 4):
for k in range(0, n - 4, 4):
C[i][j] = A[i][k] * B[k][j]
C[i + 1][j + 1] = A[i + 1][k + 1] * B[k + 1][j + 1]
C[i + 2][j + 2] = A[i + 2][k + 2] * B[k + 2][j + 2]
C[i + 3][j + 3] = A[i + 3][k + 3] * B[k + 3][j + 3]
C[i + 4][j + 4] = A[i + 4][k + 4] * B[k + 4][j + 4]
# return C
start = time.time()
matMul( A, B, C )
end = time.time()
print( f"Elapsed {end - start}" )
循环展开后,程序在 3 秒内完成,如果不展开,则需要将近 20 秒。
这是否意味着在编写 Python 代码时需要注意并手动实现这些 opt 技术?或者 Python 是否在任何特殊设置下提供优化?
循环展开很有用,因为它可以(1)减少管理循环所花费的开销;(2)在汇编级别,它通过消除分支惩罚、保持指令流水线满载等方式让处理器运行得更快。
(2) 并不适用于像 Python 这样的解释型语言实现 - 它已经在汇编级别进行了大量分支和决策。它可能会通过 (1) 为你带来好处,但我的直觉是,与解释器开销相比,这段时间往往微不足道。性能优化的黄金法则是首先测量并确认瓶颈是否在你认为的位置。
顺便说一句,你的代码有一个错误:
它处理单元格 (0, 0)、(1, 1)、(2, 2)、(3, 3) 和 (4, 4)(尽管 (4, 4) 也将在下一次迭代中处理),但不处理 (0, 1)、(0, 2)、(1, 0)...(这是性能优化黄金法则的另一个原因:优化不需要的代码很容易犯错误。)
正如@Mat所说,Python 的标准方法是使用NumPy,它使用优化的 C 实现。
以上所有内容均适用于 CPython,即标准 Python 实现。还有其他 Python 实现(如Cython)提供自己的优化;我对这些不太熟悉。
(请参阅评论以了解您的代码为什么不起作用,因为这不是真正的问题)。
正如 Josh 所说,CPython 不进行优化。因此循环展开可能有用(假设简单地不使用 Python 中的循环,例如让其
numpy
为您完成,这不是解决方案)。但是,我想指出的是,你关于解释器“即时”执行操作并因此无法优化代码的观点是错误的。
解释器绝不会只解释文本;即使是最古老的解释器,现代的解释器更不会。它们的工作原理有点像编译器,但有不同的阶段:标记化,然后是语法分析,将文本转换为标记树,然后是语义分析,例如,将标识符链接到其定义(这就是为什么即使在解释器中,与我们有时在这里读到的相反,从性能角度来看,变量名是 1 个字符还是 1000 个字符并不重要。名称在执行开始之前就从代码中消失了),如果语言是类型语言,则进行一些类型检查或评估,等等。
在那个阶段,解释器可以展开循环:它可以看到循环计数器未在循环外使用(因此,不需要保留它),并决定将树转换为通过
for
重复指令来替换)。甚至编译器与解释器之间唯一不同的阶段,即代码生成阶段,如今也经常由解释器运行,因为大多数现代解释器首先为虚拟机编译,然后运行虚拟机代码。因此,在这个阶段也可以执行一些优化,例如中间结果的分解和类型。
因此,这不会改变答案:不,CPython 并没有真正进行优化,是的,使用 CPython,展开循环有时会节省时间,这并不奇怪(当然,这取决于循环中的内容。如果在循环内部之前增加计数器并将其与范围进行比较的成本可以忽略不计,那么展开它就有点徒劳了)。但原因不是“因为 python 是一个解释器,而解释器与编译器不同,无法进行优化”。现代解释器基本上是编译器和虚拟机。甚至更传统的解释器也会解释表示程序的树,而不是文本代码,并且一些优化(如展开循环)可以在构建该树的阶段直接完成(即在
.py
读取文件后,即使未执行)。这只是因为 CPython 选择不这样做。(而 CPython 选择不这样做,因为 python 点不是最优的。如果您需要快速代码,那么您不应该用 python 编码。您应该依赖 numpy 或类似的东西,它们不是用 python 编码的。或者使用您自己的扩展,使用 Python/C Api、ctypes、numba、cython 或......)