Python timeit模块执行混淆

6

我正在尝试在Python中使用timeit模块(编辑:我们正在使用Python 3),以便在几个不同的代码流之间进行选择。在我们的代码中,我们有一系列if语句来测试字符串中字符代码的存在性,并且如果存在,则像这样替换:

if "<substring>" in str_var:
    str_var = str_var.replace("<substring>", "<new_substring>")

我们会针对不同的子字符串多次执行此操作。我们正在考虑使用这种方法,或者仅使用替换功能,如下所示:
str_var = str_var.replace("<substring>", "<new_substring>")

我们尝试使用timeit来确定哪个更快。如果上面的第一个代码块是"stmt1",第二个是"stmt2",并且我们的设置字符串如下:
str_var = '<string><substring><more_string>',

我们的timeit语句将如下所示:

我们的timeit语句将如下所示:

timeit.timeit(stmt=stmt1, setup=setup)

并且

timeit.timeit(stmt=stmt2, setup=setup)

现在,仅仅像这样在我们的两台笔记本电脑上运行它(相同的硬件,类似的处理负载),stmt1(带有if语句的语句)即使在多次运行后也比stmt2更快(0.03秒对于大约0.25秒)。

然而,如果我们定义函数来执行这两个操作(包括设置创建变量),如下所示:

def foo():
    str_var = '<string><substring><more_string>'
    if "<substring>" in str_var:
        str_var = str_var.replace("<substring>", "<new_substring>")

并且

def foo2():
    str_var = '<string><substring><more_string>'
    str_var = str_var.replace("<substring>", "<new_substring>")

并像这样运行时间测试:

timeit.timeit("foo()", setup="from __main__ import foo")
timeit.timeit("foo2()", setup="from __main__ import foo2")

没有if语句的语句(foo2)运行更快,与未经过函数化的结果相矛盾。

我们是否忽略了Timeit的工作原理?或者Python如何处理这种情况?

编辑:这是我们实际的代码:

>>> def foo():
    s = "hi 1 2 3"
    s = s.replace('1','5')

>>> def foo2():
    s = "hi 1 2 3"
    if '1' in s:
        s = s.replace('1','5')


>>> timeit.timeit(foo, "from __main__ import foo")
0.4094226634183542
>>> timeit.timeit(foo2, "from __main__ import foo2")
0.4815539780738618

与此代码相比:

>>> timeit.timeit("""s = s.replace("1","5")""", setup="s = 'hi 1 2 3'")
0.18738432400277816
>>> timeit.timeit("""if '1' in s: s = s.replace('1','5')""", setup="s = 'hi 1 2 3'")
0.02985000199987553

你能发布你的确切测试代码吗? - Blender
我基本上看到的和OP一样。当不使用foo函数方法时,if语句方法始终在0.06左右,但非if方法大约为0.3。当我使用foo函数时,在这种情况下,if语句方法大约为0.61,而非if方法大约为0.53。(这些是使用timeit对四种可能性进行10次计算的平均值。)我使用IPython和Python 2.7.3在一台相当快的桌面机器上。 - ely
我应该提到这是Python 3。我会把它加到问题中,我忘了包括它。 - CraigularB
嗯,现在我想起来了,真正奇怪的是if语句比replace语句更短。实际上,它比单独使用replace做更多的工作,因为if测试是True(所以replace在两种情况下都运行)。if可以节省时间的唯一方法是如果字符串不包含要替换的字符串,从而在replace调用本身中节省一些时间,这只有在in的时间顺序比replace快时才成立。我不知道为什么会有很大的区别,但你在非函数与if版本中使用了不同的要替换的字符串。 - jpmc26
@jpmc26 我也使用了相同的字符串。我应该复制使用所有相同字符串的版本,但即使使用相同的字符串,不使用函数的版本也会得到相同的结果。我已经更改了语句,使它们在问题中使用相同的字符串。 - CraigularB
显示剩余5条评论
2个回答

5

我想我明白了。

看看这段代码:

timeit.timeit("""if '1' in s: s = s.replace('1','5')""", setup="s = 'hi 1 2 3'")

在这段代码中,setup仅运行一次。这意味着s变成了一个"全局变量"。因此,在第一次迭代中,它被修改为hi 5 2 3,并且所有后续迭代in现在返回False
请查看此代码:
timeit.timeit("""if '1' in s: s = s.replace('1','5'); print(s)""", setup="s = 'hi 1 2 3'")

这将只打印一次“hi 5 2 3”,因为“print”是“if”语句的一部分。相反,下面的代码将在屏幕上填满大量的“hi 5 2 3”:
timeit.timeit("""s = s.replace("1","5"); print(s)""", setup="s = 'hi 1 2 3'")

因此,这里的问题是使用if测试的非函数存在缺陷,并且会给您提供错误的时间,除非您尝试测试已处理过的字符串的重复调用。 (如果这就是您要测试的内容,则您的函数版本存在缺陷。)if函数的原因并不更好,是因为它在每次迭代时都在新的字符串副本上运行replace
以下测试执行了我认为您打算执行的操作,因为它不会将replace的结果重新分配回s,使其在每次迭代中保持不变:
>>> timeit.timeit("""if '1' in s: s.replace('1','5')""", setup="s = 'hi 1 2 3'"
0.3221409016812231
>>> timeit.timeit("""s.replace('1','5')""", setup="s = 'hi 1 2 3'")
0.28558505721252914

这个更改会大大增加if测试的时间,并为非if测试增加一点时间,但我使用的是Python 2.7。如果Python 3的结果是一致的,那么这些结果表明,当字符串很少需要替换时,in可以节约大量时间。如果它们通常需要替换,那么in看起来会花费一点时间。

谢谢!我认为这是最有意义的。实际上,我们还试图测试赋值运算符(因为获取替换后的值并对其进行以下操作对于我们如何处理数据非常重要)。我运行了以下内容:`>>> s = """ s = 'hi 1 2 3' if '1' in s: s = s.replace('1','5') """
timeit.timeit(s) 0.26153747701027896 s2 = """ s = 'hi 1 2 3' s = s.replace('1','5') """ timeit.timeit(s2) 0.24757718200271484`,它似乎证实了您的发现和我们之前的发现。谢谢!
- CraigularB

0

通过查看反汇编代码,使其变得更加奇怪。第二个块具有if版本(在我使用timeit时比OP示例中的速度更快)。

然而,通过查看操作码,它纯粹似乎有7个额外的操作码,从第一个BUILD_MAP开始,并涉及一个额外的POP_JUMP_IF_TRUE(可能是为了if语句本身的检查)。在此之前和之后,所有代码都是相同的。

这表明,在if语句中构建和执行检查以某种方式减少了计算时间,因此可以在调用replace时进行检查。我们如何查看不同操作码的具体计时信息?

In [55]: dis.disassemble_string("s='HI 1 2 3'; s = s.replace('1','4')")
          0 POP_JUMP_IF_TRUE 10045
          3 PRINT_NEWLINE
          4 PRINT_ITEM_TO
          5 SLICE+2
          6 <49>
          7 SLICE+2
          8 DELETE_SLICE+0
          9 SLICE+2
         10 DELETE_SLICE+1
         11 <39>
         12 INPLACE_MODULO
         13 SLICE+2
         14 POP_JUMP_IF_TRUE 15648
         17 SLICE+2
         18 POP_JUMP_IF_TRUE 29230
         21 LOAD_NAME       27760 (27760)
         24 STORE_GLOBAL    25955 (25955)
         27 STORE_SLICE+0
         28 <39>
         29 <49>
         30 <39>
         31 <44>
         32 <39>
         33 DELETE_SLICE+2
         34 <39>
         35 STORE_SLICE+1

In [56]: dis.disassemble_string("s='HI 1 2 3'; if '1' in s: s = s.replace('1','4')")
          0 POP_JUMP_IF_TRUE 10045
          3 PRINT_NEWLINE
          4 PRINT_ITEM_TO
          5 SLICE+2
          6 <49>
          7 SLICE+2
          8 DELETE_SLICE+0
          9 SLICE+2
         10 DELETE_SLICE+1
         11 <39>
         12 INPLACE_MODULO
         13 SLICE+2
         14 BUILD_MAP        8294
         17 <39>
         18 <49>
         19 <39>
         20 SLICE+2
         21 BUILD_MAP        8302
         24 POP_JUMP_IF_TRUE  8250
         27 POP_JUMP_IF_TRUE 15648
         30 SLICE+2
         31 POP_JUMP_IF_TRUE 29230
         34 LOAD_NAME       27760 (27760)
         37 STORE_GLOBAL    25955 (25955)
         40 STORE_SLICE+0
         41 <39>
         42 <49>
         43 <39>
         44 <44>
         45 <39>
         46 DELETE_SLICE+2
         47 <39>
         48 STORE_SLICE+1

如果我理解正确的话,那么唯一的区别就是第二个指令集中插入了14到24行。是这样吗? - jpmc26
是的。但看起来问题出在timeit与安装设置功能的使用方式上,而不是代码本身。 - ely

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