为什么“if x:”比“if x>0”快得多?康威生命游戏

4

我正在使用 Python 3 制作康威生命游戏,有个名为 updateboard 的函数,可以根据邻居数量(0 到 8)决定细胞的死亡和出生,这些邻居数量存储在 self.neighbors 中。函数如下:

def updateboard(self):
        """
        Updates the board state
        """
        alive = np.zeros((self.cellsize, self.cellsize), dtype=np.bool)     # board state for next loop
        
        # iterate cells
        for y in range(self.cellsize):
            for x in range(self.cellsize):
                if self.neighbors[x, y] > 0:    # only look at cells with more than 1 neighbors
                    if self.board[x, y]:        # cell is currently alive
                        alive[x, y] = True if self.neighbors[x, y] in (2, 3) else False
                    else:                       # cell is currently dead
                        alive[x, y] = True if self.neighbors[x, y] == 3 else False
        
        # update states
        self.updateneighbors(self.board, alive)
        self.board = alive

为了避免冗余的检查,我会先检查该单元格中的self.neighbors是否大于0,然后再确定该单元格是生还是死。
我尝试了不同的优化方法,发现将if self.neighbors[x, y] > 0更改为if self.neighbors[x, y]可以显著加快函数运行速度。
运行Python分析器显示,这一个改变让函数的运行时间几乎提升了6倍。 之前的代码:
ncalls  tottime  percall  cumtime  percall filename:lineno(function)
   459   12.465    0.027   13.916    0.030 logic.py:55(updateboard)

之后

ncalls  tottime  percall  cumtime  percall filename:lineno(function)
   460    1.619    0.004    3.067    0.007 logic.py:55(updateboard)

我试图在网上寻找解释,并发现了许多类似的问题,但尚未找到针对这个具体问题的答案。 我既感到困惑又感到惊讶,因为这一个小小的改变竟然造成了如此大的差异,如果有人能帮助我解释一下,我将非常感激。


如果您在询问实现细节方面的问题,您应该始终命名解释器和版本。 - Klaus D.
谢谢。我更新了我的帖子,以指定我正在使用的Python版本。 - Orbital
你的问题归结为“为什么if x:if x>0:更快。”始终提出最小可能的问题 - 较小的问题更容易理解和回答。 - DYZ
3个回答

4
你正通过逐个迭代NumPy数组元素来工作,而不是将工作推入优化的NumPy C级别代码中。这根本没有充分利用NumPy的威力。这就像你在走路去商店时手动拖着汽车,而不是开车一样。
NumPy并不为单个元素操作而设计。它支持它们,但前提是单个元素操作很少见。对于大多数关于单个元素的操作(如>),NumPy存在巨大的开销。在使用普通Python列表和标量时不会发生调度、类型转换和包装对象分配等操作。
一个简单的实验表明,这种开销完全不同于处理普通Python整数时所发生的情况:
In [1]: x = 1

In [2]: %timeit if x: pass
26.1 ns ± 0.0316 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

In [3]: %timeit if x > 1: pass
35.6 ns ± 0.315 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

In [4]: import numpy

In [5]: x = numpy.int64(1)

In [6]: %timeit if x: pass
26.8 ns ± 0.0422 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

In [7]: %timeit if x > 1: pass
203 ns ± 7.63 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

相较于numpy.int64> 1对于int的额外负担要小得多。

请注意,在if x中涉及到的隐式布尔解释对于NumPy来说比x > 1简单得多。没有第二个参数要分派或转换,而C级别的nb_bool钩子不需要创建一个包装对象。NumPy可以几乎与普通Python标量一样快地执行此操作。


为了真正加速您的代码,您应该使用数组而不是元素进行操作:

alive = (self.neighbors == 3) | ((self.neighbors == 2) & self.board)

NumPy将能够直接在C级别上循环遍历数组缓冲区,从而产生更低的开销。


0
请注意,此行为可能会根据您使用的Python解释器或CPU架构而发生更改。
一般来说,x86 CPU具有一个特殊位,用于在算术运算后检查值是否为零,称为零标志。当您想要进行相等性检查时,就会使用它,例如: if x == 3 在汇编中,它将是:
mov ecx, $1
mov edx, 3
cmp ecx, edx
je equal
; if it did not jump to the label equal,
; then this means ecx and edx are not equal.
equal:
; if it jumped here, then this means ecx and edx are equal

详细信息请查看

if x < 3:

它将产生非常相似的汇编代码,因为有特定的X86指令用于不等比较,并且在底层使用了负标志

详细信息请查看

但正如我所指出的,执行哪个汇编取决于解释器和处理器架构。请注意,即使处理器的内部优化也可能会产生巨大影响,例如:分支预测,请参见:为什么处理排序数组比处理未排序数组快?

所以一般的答案是:除非你检查执行的确切汇编代码,否则你永远不会知道实际发生了什么,即使你有汇编代码,它也可能不是实际执行的内容。总之,你不应该为这种性能问题烦恼,也不应该因此更改你的代码。如果你遇到这样的性能问题,请尝试升级你的解释器。如果低级性能是一个巨大的因素,也许你不应该使用Python,而应该转换到另一种语言,在那里你可以更直接地改变低级行为(例如:C/C++)。

如果你对CPU如何执行你认为已经提供的代码有兴趣,Herb Sutter在这个主题上有一个非常好的演示。你可能特别感兴趣演示的第一部分:

https://www.youtube.com/watch?v=A8eCGOqgvH4

https://www.youtube.com/watch?v=KeLBd2EJLOU


2
这是一个非常“编译”的观点 - 它与Python的工作方式完全不同。 - user2357112

0

显式比较需要两个额外操作:加载常量0(LOAD_CONST)并将其与您的表达式进行比较(COMPARE_OP)。

dis.dis('if x>0: pass')
  1           0 LOAD_NAME                0 (x)
              2 LOAD_CONST               0 (0)
              4 COMPARE_OP               4 (>)
              6 POP_JUMP_IF_FALSE        8
        >>    8 LOAD_CONST               1 (None)
             10 RETURN_VALUE

dis.dis('if x: pass')
  1           0 LOAD_NAME                0 (x)
              2 POP_JUMP_IF_FALSE        4
        >>    4 LOAD_CONST               0 (None)
              6 RETURN_VALUE

操作POP_JUMP_IF_FALSE会在栈顶是布尔值False时跳转。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接