迭代列表的奇怪速度差异

2024-05-17

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

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。

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

  • 分支预测 https://stackoverflow.com/q/11227809/16759116:我没有任何可以产生影响的分支。
  • 缓存未命中 https://stackoverflow.com/q/69950010/16759116:不应该发生,因为我只有两个值(而且我什至没有对它们做任何事情)。
  • 垃圾收集 https://stackoverflow.com/q/63757763/16759116: 这里不涉及。
  • 这些都不能解释opposite无论如何,计时方法之间的速度差异。

我使用的是 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()

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

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

然后在default_timer分支替换

for _ in a1: pass

with 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 循环目标_是一个全局模块,因此在每次迭代时更新它都需要字典查找和存储。但timeit用传递给它的东西构建一个函数,所以在代码中timeit runs, _是一个局部变量,而且速度更快STORE_FAST索引存储就足够了。

功能f()引入,以便在这两种情况下都在局部变量上完成“繁重的工作”。

此处计时的代码执行的操作非常少,因此字典查找和带有 int 的索引向量操作之间的差异可能非常显着

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

迭代列表的奇怪速度差异 的相关文章