脚本 运行 循环中代码与被调用函数中代码的时间差
Script run time difference for code in loop vs code in called function
我运行 2个函数。它们都有用于执行指令的 for 循环。这两个功能完成相同的任务,但一个需要更长的时间。
函数 1 执行并自包含,执行任务 A。
f1:
For x in X:
do task a
Function 2执行并调用Function 3。Function 3执行TaskA
f2:
For x in X:
call function 3
f3:
do task a
为什么函数 2 的执行时间通常是函数 1 的 10 倍?
编辑:之前的措辞让人们感到困惑。
另一个因素可能是在调用 TaskA
之前完成的 "preparation" / 设置。可能在 f1
中,您在 for
循环之前完成了一次,然后在 f3
中完成,因此它会被 f2
中的每个 x in X
调用而不是一开始就一次。没有任何真正的代码,这很难说。
至于为每个 x
调用 f3
的潜在复杂性,这不太可能是 10 倍慢的原因。
只有在 pass
的过度简化示例中,我们才会看到这种行为。让我们以 f1
、f2
和 f3
的这 3 个错误版本为例:
>>> def f1():
... for x in X:
... pass
...
>>> def f2():
... for x in X:
... f3()
...
>>> def f3():
... pass
...
使用 dis
,下面是 f1
的字节码:
>>> dis.dis(f1)
2 0 SETUP_LOOP 14 (to 17)
3 LOAD_GLOBAL 0 (X)
6 GET_ITER
>> 7 FOR_ITER 6 (to 16)
10 STORE_FAST 0 (x)
3 13 JUMP_ABSOLUTE 7
>> 16 POP_BLOCK
>> 17 LOAD_CONST 0 (None)
20 RETURN_VALUE
...对比 f2
:
>>> dis.dis(f2)
2 0 SETUP_LOOP 21 (to 24)
3 LOAD_GLOBAL 0 (X)
6 GET_ITER
>> 7 FOR_ITER 13 (to 23)
10 STORE_FAST 0 (x)
3 13 LOAD_GLOBAL 1 (f3)
16 CALL_FUNCTION 0
19 POP_TOP
20 JUMP_ABSOLUTE 7
>> 23 POP_BLOCK
>> 24 LOAD_CONST 0 (None)
27 RETURN_VALUE
除了 CALL_FUNCTION
和 POP_TOP
外,它们看起来几乎相同。但是,它们与 timeit
:
有很大不同
>>> X = range(1000) # [0, 1, 2, ...999]
>>>
>>> import timeit
>>> timeit.timeit(f1)
10.290941975496747
>>> timeit.timeit(f2)
81.18860785875617
>>>
现在是时间的 8 倍,但不是因为调用函数 慢 而是因为在 f1
的 for 循环中只做 pass
是 非常快,尤其是当每次调用一个函数时什么都不做。所以希望你 不是 使用这些作为例子来找到 out/wonder 为什么。
现在,如果你真的在任务中做了一些事情,比如说 x * x
那么你会发现两者之间的 timing/performance 差异变小了:
>>> def f1():
... for x in X:
... _ = x*x
...
>>> def f2():
... for x in X:
... _ = f3(x) # didn't pass in `x` to `f3` in the previous example
...
>>> def f3(x):
... return x*x
...
>>> timeit.timeit(f1)
38.76545268807092
>>> timeit.timeit(f2)
113.72242594670047
>>>
现在只有 2.9x 时间了。导致速度缓慢的不是函数调用(是的,有一些开销),而是你在该函数中所做的与 pass
相比,这对总时间产生了影响。
如果你把两个地方的 _ = x * x
都替换成 print x * x
,这相当于 "slow",而只用 X = range(5)
:
>>> timeit.timeit(f1, number=10000)
3.640433839719143
>>> timeit.timeit(f2, number=10000)
3.6921612171574765
现在他们的表现差异小了很多。
因此,请使用真实代码进行实际检查,而不仅仅是简单的伪代码分析。空调用可能看起来更快,但与函数中的代码较慢的东西相比,这种开销确实很小。
我运行 2个函数。它们都有用于执行指令的 for 循环。这两个功能完成相同的任务,但一个需要更长的时间。
函数 1 执行并自包含,执行任务 A。
f1:
For x in X:
do task a
Function 2执行并调用Function 3。Function 3执行TaskA
f2:
For x in X:
call function 3
f3:
do task a
为什么函数 2 的执行时间通常是函数 1 的 10 倍?
编辑:之前的措辞让人们感到困惑。
另一个因素可能是在调用 TaskA
之前完成的 "preparation" / 设置。可能在 f1
中,您在 for
循环之前完成了一次,然后在 f3
中完成,因此它会被 f2
中的每个 x in X
调用而不是一开始就一次。没有任何真正的代码,这很难说。
至于为每个 x
调用 f3
的潜在复杂性,这不太可能是 10 倍慢的原因。
只有在 pass
的过度简化示例中,我们才会看到这种行为。让我们以 f1
、f2
和 f3
的这 3 个错误版本为例:
>>> def f1():
... for x in X:
... pass
...
>>> def f2():
... for x in X:
... f3()
...
>>> def f3():
... pass
...
使用 dis
,下面是 f1
的字节码:
>>> dis.dis(f1)
2 0 SETUP_LOOP 14 (to 17)
3 LOAD_GLOBAL 0 (X)
6 GET_ITER
>> 7 FOR_ITER 6 (to 16)
10 STORE_FAST 0 (x)
3 13 JUMP_ABSOLUTE 7
>> 16 POP_BLOCK
>> 17 LOAD_CONST 0 (None)
20 RETURN_VALUE
...对比 f2
:
>>> dis.dis(f2)
2 0 SETUP_LOOP 21 (to 24)
3 LOAD_GLOBAL 0 (X)
6 GET_ITER
>> 7 FOR_ITER 13 (to 23)
10 STORE_FAST 0 (x)
3 13 LOAD_GLOBAL 1 (f3)
16 CALL_FUNCTION 0
19 POP_TOP
20 JUMP_ABSOLUTE 7
>> 23 POP_BLOCK
>> 24 LOAD_CONST 0 (None)
27 RETURN_VALUE
除了 CALL_FUNCTION
和 POP_TOP
外,它们看起来几乎相同。但是,它们与 timeit
:
>>> X = range(1000) # [0, 1, 2, ...999]
>>>
>>> import timeit
>>> timeit.timeit(f1)
10.290941975496747
>>> timeit.timeit(f2)
81.18860785875617
>>>
现在是时间的 8 倍,但不是因为调用函数 慢 而是因为在 f1
的 for 循环中只做 pass
是 非常快,尤其是当每次调用一个函数时什么都不做。所以希望你 不是 使用这些作为例子来找到 out/wonder 为什么。
现在,如果你真的在任务中做了一些事情,比如说 x * x
那么你会发现两者之间的 timing/performance 差异变小了:
>>> def f1():
... for x in X:
... _ = x*x
...
>>> def f2():
... for x in X:
... _ = f3(x) # didn't pass in `x` to `f3` in the previous example
...
>>> def f3(x):
... return x*x
...
>>> timeit.timeit(f1)
38.76545268807092
>>> timeit.timeit(f2)
113.72242594670047
>>>
现在只有 2.9x 时间了。导致速度缓慢的不是函数调用(是的,有一些开销),而是你在该函数中所做的与 pass
相比,这对总时间产生了影响。
如果你把两个地方的 _ = x * x
都替换成 print x * x
,这相当于 "slow",而只用 X = range(5)
:
>>> timeit.timeit(f1, number=10000)
3.640433839719143
>>> timeit.timeit(f2, number=10000)
3.6921612171574765
现在他们的表现差异小了很多。
因此,请使用真实代码进行实际检查,而不仅仅是简单的伪代码分析。空调用可能看起来更快,但与函数中的代码较慢的东西相比,这种开销确实很小。