当前位置: 首页 > 面试题库 >

为什么使用中间变量的代码要比不使用中间变量的代码快?

柴岳
2023-03-14
问题内容

我遇到这种奇怪的行为,但无法解释。这些是基准:

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.x中重现,尽管程度较小。

运行Windows 7,CPython 3.5.1,Intel i7 3.40 GHz,64位OS和Python。似乎我尝试使用Python
3.5.0在Intel i7 3.60 GHz上运行的另一台机器无法重现结果。

使用具有timeit.timeit()10000个循环的相同Python进程运行分别产生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 。

因此,这个Heisenbug变得非常有趣。我决定运行该命令,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系统调用比使用中间变量的代码多近1000倍。

对于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通话似乎是从功能来_PyObject_ArenaMmapObjects/obmalloc.c;
obmalloc.c还包含宏ARENA_SIZE,这是#defined至是(256 << 10)(即262144);
类似地munmap匹配_PyObject_ArenaMunmapfrom obmalloc.c

obmalloc.c

在Python 2.5之前,竞技场从未被使用过free()。从Python
2.5开始,我们确实尝试使用free()竞技场,并使用一些温和的启发式策略来增加最终释放竞技场的可能性。

因此,这些试探法以及Python对象分配器在清空后立即释放这些空闲区域的事实导致python3 -mtimeit 'tuple(range(2000)) == tuple(range(2000))'触发病理行为,其中一个256 kiB内存区域被重新分配并重复释放。这种分配情况与mmap/
munmap,这是因为他们的系统调用相对昂贵的-而且,mmapMAP_ANONYMOUS要求新映射的页面必须清零-
尽管Python的也不会在意。

该行为在使用中间变量的代码中不存在,因为它使用了 更多的
内存,并且由于仍在其中分配了一些对象,因此无法释放任何内存空间。那是因为timeit它将使其循环成环

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

现在的行为是,无论ab将保持约束,直到他们重新分配*,所以在第二次迭代,tuple(range(2000))将分配一个3元组,并分配a = tuple(...)将降低旧的元组的引用计数,导致它被释放,并提高新元组的引用计数;然后发生同样的事情b。因此,在第一次迭代之后,这些元组中始终至少有2个(如果不是3个),因此不会发生颠簸。

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

有人问为什么timeit禁用垃圾收集时会发生这种情况。确实是这样timeit做的:

注意

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

但是,Python的垃圾收集器仅用于回收 循环垃圾 ,即引用形成循环的对象的集合。这里不是这种情况。而是当引用计数降至零时立即释放这些对象。



 类似资料:
  • 我在JMeter中有一个Config元素,特别是用户定义的变量。 我有变量用户与值贾斯汀,我怎么能在groovy代码(一个JSR223断言)中使用这个变量?

  • 问题内容: 我是PHP新手(仍然),并且继续学习。 我经常必须检索某个变量并访问其属性。 我希望一次检索一次,然后在需要时在同一文件中但在不同块中使用它 但是我怀疑不能在块之间共享,因为它不是全局的。通常的做法是什么? 问题答案: 您在php代码块中放置了太多含义。 这不是全球性的事情。 这些块属于同一PHP脚本。这只是输出HTML的一种简洁方法,仅此而已。您可以用回显HTML代替它,不会有丝毫差

  • 问题内容: 为什么说java中的静态变量尽量不要使用? 问题答案: 静态变量表示全局状态。这很难推理,也很难测试:如果创建对象的新实例,则可以在测试中推断其新状态。如果我使用的代码使用的是静态变量,则它可能处于任何状态-任何事情都可能对其进行修改。 我可以继续进行一段时间,但是要考虑的更大概念是,事物的范围越紧密,就越容易进行推理。我们善于思考小事情,但是如果没有模块化,就很难对一百万行系统的状态

  • 我正在使用SQL Developer并编写此PL/SQL代码。但当我给B作为选项时,我得到了一个错误。请帮忙。 错误报告:

  • 问题内容: 我正在尝试从Java代码中使用代码,原因是它在Eclipse中不起作用,而Scala则可以。但我无法获得方法 之所以能够正常工作,是因为它似乎期望第二个参数使用a,而且我看不到如何在Java中创建a 。我该如何解决? 我尝试过的事情: 1)使用null -获得奖励。 2)替换为with ,但是javac报告各种错误,例如没有方法。 3)在包对象中使用该对象,但此处建议的语法为,但无法解

  • 问题内容: 我有几个字段,每个字段都是这样的: 通过使用带有计数器的循环,我希望能够说出fieldx。其中x是该循环中计数器的值。这意味着如果我的数组中有6个条目,则将为fields1-field6提供值。 是否可以使用fieldx? 问题答案: 您可以使用反射来完成此操作,但是通常最好在数组中声明字段。代替: 你可以这样做: 然后,您可以遍历数组来设置值: