为什么 2x2 循环展开在 python 中运行速度较慢(但在使用 jit nopython 编译时则不然)

问题描述 投票:0回答:1

这些函数提供相同的结果(假设是偶数长度数组)。然而,当输入 10,000,000 个浮点数时,2x2 展开函数的运行速度会慢 30%。当我将函数更改为在 nopython 模式下运行时,我发现展开函数的速度大约提高了 2.5 倍。

为什么普通Python无法通过循环展开获得加速?这两个版本的代码编译和运行方式有什么根本区别?

# @jit(nopython=True)
def func1(array):
    numSum = 0

    for i in range(array.shape[0]):
        numSum += array[i]

    return numSum

# 2x2 unrolled
# @jit(nopython=True)
def unrolledFunc1(array):
    numSum0 = 0
    numSum1 = 0 

    for i in range(0, array.shape[0]-1, 2):
        numSum0 += array[i] 
        numSum1 += array[i+1] 

    numSum = numSum0 + numSum1 

    return numSum
python optimization jit loop-unrolling
1个回答
0
投票

首先,你循环展开错误(搜索

duff's device
)。

其次,Python 在访问数组元素时有很多开销(越界检查、环绕检查等),这是大部分程序时间花费的地方,而不是实际的加法。

第三,您可以使用

dis.dis
或其他Python反汇编工具来查看生成的字节码,并查看展开的循环和正常循环之间到底有什么区别(在大多数情况下没有太大区别,因为大部分CPU时间都被“浪费了”) “执行检查)。

最后,循环展开不会对现代硬件有任何改进!您可能会感到惊讶,但 for 循环实际上是 while 循环,在机器代码中类似于:

loop_begin:
    i = 0
    target = 100
repeated_section:
    if i < target:
        # DO SOME PROCESSING
        i = i+1
        jump to repeated_section

不信,搜索

Python while else
。现代硬件经过调整可以预测分支并提前进行尽可能多的处理。由于在 for 循环中,分支的可预测性非常好(除了最后一次迭代之外,分支总是正确的),因此分支预测器会在您没有意识到的情况下优化您的循环。我不确定你到底是如何运行基准测试的,但我使用了 Cython 并编写了一堆循环展开的变体(使用 Duff 的设备和正常循环手动展开 100 个循环),并且它们都有非常相似的运行时间(在彼此的错误),特别是如果您使用正确的编译器标志(Linux 上的 GCC 为 -O3 或 -Ofast,Windows 上的 Visual Studio C++ 编译器为 /O2i)。

如果您使用黑魔法(@jit),我不会担心其生成的代码的性能差异。您应该尝试让 jit 编译器理解您想要执行的操作,以便它可以为您进行优化。话虽这么说,如果您真正尝试优化代码而不仅仅是尝试 jit 并检查哪些内容存在问题,那么您需要控制实际的源代码、编译器标志等。在运行基准测试之前,您可以了解您的更改做了什么而且您必须使用分析器来查看代码中的实际瓶颈是什么,而不是猜测和检查。

© www.soinside.com 2019 - 2024. All rights reserved.