我遇到过这种奇怪的行为,但没能解释清楚.这些基准是:

py -3 -m timeit "tuple(range(2000)) == tuple(range(2000))"
10000 loops, best of 3: 97.7 usec per loop
py -3 -m timeit "a = tuple(range(2000));  b = tuple(range(2000)); a==b"
10000 loops, best of 3: 70.7 usec per loop

为什么使用变量赋值进行比较比使用带有临时变量的一行程序快27%以上?

根据Python文档,垃圾收集在timeit期间被禁用,所以不能这样.这是某种优化吗?

结果也可以在Python 2中重现.虽然程度较轻.

运行Windows 7/10、CPython 3.5.1一直到3.10.1、Intel i7 3.40 GHz、64位操作系统和Python.我试着在英特尔i7 3.60 GHz和Python 3.5.0上运行的另一台机器似乎没有重现结果.


使用相同的Python进程运行timeit.timeit()@timeit.timeit()00循环分别产生0.703和0.804.尽管程度较轻,但仍能显示.(~12.5%)

推荐答案

我的结果与你的类似:在Python 3.4中,使用中间变量的代码始终至少快10-20%.然而,当我在同一个Python 3.4解释器上使用IPython时,我得到了以下结果:

In [1]: %timeit -n10000 -r20 tuple(range(2000)) == tuple(range(2000))
10000 loops, best of 20: 74.2 µs per loop

In [2]: %timeit -n10000 -r20 a = tuple(range(2000));  b = tuple(range(2000)); a==b
10000 loops, best of 20: 75.7 µs per loop

值得注意的是,当我在命令行中使用-mtimeit时,我从未设法接近前者的74.2µs.

所以这只海森堡是很有趣的东西.我决定用strace来运行这个命令,确实有些可疑之处:

% strace -o withoutvars python3 -m timeit "tuple(range(2000)) == tuple(range(2000))"
10000 loops, best of 3: 134 usec per loop
% strace -o withvars python3 -mtimeit "a = tuple(range(2000));  b = tuple(range(2000)); a==b"
10000 loops, best of 3: 75.8 usec per loop
% grep mmap withvars|wc -l
46
% grep mmap withoutvars|wc -l
41149

这就是造成这种差异的一个很好的原因.不使用变量的代码导致调用mmap系统调用的次数几乎是使用中间变量的调用次数的mmap0倍.

对于一个256k的区域来说,withoutvarsmmap/munmap;这些同样的台词一遍又一遍地重复:

mmap(NULL, 262144, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f32e56de000
munmap(0x7f32e56de000, 262144)          = 0
mmap(NULL, 262144, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f32e56de000
munmap(0x7f32e56de000, 262144)          = 0
mmap(NULL, 262144, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f32e56de000
munmap(0x7f32e56de000, 262144)          = 0

mmap调用似乎来自Objects/obmalloc.c的函数_PyObject_ArenaMmapobmalloc.c还包含宏ARENA_SIZE,其#defined为(256 << 10)(即262144);同样,munmapobmalloc.c中的_PyObject_ArenaMunmap相匹配.

obmalloc.c表示

在Python 2.5之前,竞技场从来都不是free()'ed.从Python 2.5开始,

因此,这些启发式方法以及Python对象分配器在这些空闲区域清空后立即释放它们的事实导致python3 -mtimeit 'tuple(range(2000)) == tuple(range(2000))'个触发病理行为,其中一个256kib的内存区域被重新分配并反复释放;这种分配发生在mmap/munmap上,这是相对昂贵的,因为它们是系统调用——此外,mmapMAP_ANONYMOUS要求新映射的页面必须归零——尽管Python不在乎.

在使用中间变量的代码中不存在这种行为,因为它使用的内存略为more,并且无法释放内存,因为其中仍分配了一些对象.这是因为timeit将使其成为一个循环,这与

for n in range(10000)
    a = tuple(range(2000))
    b = tuple(range(2000))
    a == b

现在的行为是,ab都将保持绑定,直到它们被*重新分配,因此在第二次迭代中,tuple(range(2000))将分配第三个元组,分配a = tuple(...)将减少旧元组的引用计数,导致其被释放,并增加新元组的引用计数;b也一样.因此,在第一次迭代之后,这些元组中至少有2个元组,如果不是3个,那么就不会发生抖动.

最值得注意的是,不能保证使用中间变量的代码总是更快——事实上,在某些设置中,使用中间变量可能会导致额外的mmap个调用,而直接比较返回值的代码可能会更好.


有人问为什么timeit禁用垃圾收集时会发生这种情况.的确,timeit does it:

Note

默认情况下,timeit()会在计时期间临时关闭垃圾收集.这种方法的优点是,它使独立计时更具可比性.这个缺点是GC可能是被测量函数性能的重要组成部分.如果是这样,GC可以作为设置字符串中的第一条语句重新启用.例如:

然而,Python的垃圾收集器仅用于回收cyclic garbage个对象,即引用形成循环的对象集合.这里的情况并非如此;相反,当引用计数降至零时,这些对象会立即被释放.

Python-3.x相关问答推荐

Python将类实例变量转换为嵌套 struct

为什么在Python中使用RANDINT函数时会出现此TypeError?

十进制浮点数到整型的转换错误

Select 作为 MultiIndex 一部分的两个 DatetimeIndex 之间的行

我可以设置树视图层次 struct 按钮吗?

将水平堆叠的数据排列成垂直

Python 列表求和所有出现的保留顺序

使用gekko python的混合整数非线性规划

python 3.10.5 中可能存在的错误. id 函数工作不明确

如何在Pandas 中按条件计算分组?

Seaborn:注释线性回归方程

Python - For 循环数百万行

如何配置 Atom 以运行 Python3 脚本?

IronPython 3 支持?

导入 python 模块而不实际执行它

Python 3 - Zip 是 pandas 数据框中的迭代器

异常被忽略是什么类型的消息?

将 Python SIGINT 重置为默认信号处理程序

在 Keras 中训练神经网络的零精度

字典理解中的操作顺序