【问题标题】:Weird speed differences for iterating lists迭代列表的奇怪速度差异
【发布时间】:2021-12-27 18:04:34
【问题描述】:

我创建了两个重复两个不同值的长列表。在第一个列表中,值交替出现,在第二个列表中,一个值出现在另一个值之前:

a1 = [object(), object()] * 10**6
a2 = a1[::2] + a1[1::2]

然后我迭代它们,对它们什么都不做:

for _ in a1: pass
for _ in a2: pass

两者中哪一个迭代得更快?取决于我如何测量!我用每种计时方法进行了 50 场比赛:

timing method: timeit.timeit
  list a1 won 50 times
  list a2 won 0 times

timing method: timeit.default_timer
  list a1 won 0 times
  list a2 won 50 times

为什么这两种计时方法给我的结果完全相反?为什么这两个列表之间存在速度差异?我希望两次更像 25-25,而不是 50-0 和 0-50。

以前类似问题的原因以及我认为他们在这里不负责任的原因:

  • branch prediction:我没有任何可以产生影响的分支。
  • cache misses:不应该发生,因为我只有两个值(而且我什至没有对它们做任何事情)。
  • garbage collection:这里不涉及。
  • 无论如何,这些都不能解释计时方法之间的相反速度差异。

我使用的是 Python 3.10,在弱 Windows 笔记本电脑和强 Linux Google Compute Engine 实例上的结果相同。

完整代码:

from timeit import timeit, default_timer

a1 = [object(), object()] * 10**6
a2 = a1[::2] + a1[1::2]

for method in 'timeit', 'default_timer':
    wins1 = wins2 = 0

    for _ in range(50):

        time1 = time2 = float('inf')
        for _ in range(5):

            if method == 'timeit':
                t1 = timeit('for _ in a1: pass', 'from __main__ import a1', number=1)
                t2 = timeit('for _ in a2: pass', 'from __main__ import a2', number=1)
            else:
                start = default_timer();
                for _ in a1: pass
                end = default_timer()
                t1 = end - start

                start = default_timer();
                for _ in a2: pass
                end = default_timer()
                t2 = end - start

            time1 = min(time1, t1)
            time2 = min(time2, t2)

        wins1 += time1 < time2
        wins2 += time2 < time1

    print(f'timing method: timeit.{method}')
    print(f'  list a1 won {wins1} times')
    print(f'  list a2 won {wins2} times')
    print()

【问题讨论】:

  • 好的,在完整阅读了你的问题之后,可能是为了澄清一个没有比另一个快 factor 50,而是在与他们比赛之后互相50次,一个赢了50次,另一个赢了0次。
  • 您可以通过选择随机进行测量的顺序来减少系统影响。那么问题可能不是“为什么 x 比 y 快”而是“为什么第一次测量比第二次测量快”
  • @mkrieger1 谢谢,已更改。现在清楚了吗?
  • 我想立即指出一件事,以防这不明显 - 您的列表不是交替值,而是对两个对象的引用。您在内存中有两个对象,并且您的两个列表都只包含指向这些相同对象的引用。为什么这些引用的顺序会导致不同的计时器方法测量不同,我不确定,我需要深入研究这两种方法。
  • @JérômeRichard(续)参见this answer 另一个类似问题。这还观察到单个重复值比交替重复的两个值慢,并推测“管道停顿”可能是罪魁祸首。如果这是原因之一,那么我会在这里了解有关 that 的知识,而不仅仅是有关 CPython 的知识。

标签: python list performance iteration


【解决方案1】:

不是“答案”,而是线索:添加

def f(a):
    for _ in a: pass

然后在default_timer分支替换

for _ in a1: pass

使用f(a1),对于a2 的迭代也是如此。

那时我看到了两件事:

  1. 输出变化更均匀
timing method: timeit.timeit

  list a1 won 50 times
  list a2 won 0 times

timing method: timeit.default_timer
  list a1 won 50 times
  list a2 won 0 times
  1. timeit 的运行速度不再比default_timer 快,

#2 非常明显,可能很难注意到原因;-) 在原始版本中,for 循环目标 _ 是一个全局模块,因此在每次迭代时更新它需要 dict 查找和存储。但是timeit 会根据传递给它的内容构建一个函数,因此在timeit 运行的代码中,_ 是一个局部变量,而更快的STORE_FAST 索引存储就足够了。

引入了函数f(),以便在两种情况下都对局部变量进行“繁重的提升”。

这里计时的代码做的很少,以至于 dict 查找和 index-a-vector-with-an-int 操作之间的差异可能非常显着

【讨论】:

  • 我认为这是对问题的完整答案。好工作。请参阅this code 以测试使用全局变量而不是本地变量对性能的影响。组装差异可见here。欲了解更多信息,请阅读Performance with global variables vs local。解决此问题的一种解决方案应该是从 main 函数中调用代码。
  • @JérômeRichard 不完整,仅涵盖“简单”部分。仍然不清楚为什么交替列表使用局部变量更快但使用全局变量更慢。
  • @nocomment 本地版本使用STORE_FAST 指令,它比STORE_NAME 全局替代方案更快。 xxx_FAST 版本更快,因为 CPython 的内部结构:局部变量存储在可以直接由整数索引的向量中,而全局变量则从慢速字典中获取,该字典通常是带有字符串键的哈希图(在答案)。如果您现在想了解更多信息,可以阅读this(关于 CPython 代码)。
  • @JérômeRichard 我知道这一切。你错过了这个问题。让我换个说法:为什么 a2 slowera1 使用 local _ 同时使用两者,以及为什么 a2 fastera1 使用 global _ 同时使用两者?
  • @nocomment,并不是每个人都能看到我们看到的相同结果。这意味着我们可能会看到正在使用的 CPU 特有的效果。在这种情况下,Python 是地球上追求它的最糟糕的语言之一 :-) 汇编程序将是理想的,或者最糟糕的是非常简单的 C 代码。在确定“第一种计时方法导致时间快了大约 3 倍(!)”之后,我对追求这一点的兴趣就结束了,因为这两种计时方法实际上运行了不同的代码 - Python级别- 以极其相关的方式(正如这个答案所阐明的那样)。
猜你喜欢
  • 1970-01-01
  • 2013-09-10
  • 2012-05-11
  • 2013-07-06
  • 2013-04-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多