jupyterLab %%timeit 与 python timeit 不一致
jupyterLab %%timeit not consistent with python timeit
我想测试类型提示是否会影响代码执行到 运行 的时间。它可能会增加一点点时间,因为编译器必须忽略它,这需要时间,但我想看看这是多么微不足道。
为此,我在 jupyterLab 上执行了这个:
然而,我的一个同学在没有 jupyterLab 的情况下尝试了这个,发现了这个:
有人能解释为什么会这样吗?
使用的函数:
def func(a, b):
return a + b
func(6, 7)
和:
def func(a: int, b: int) -> int:
return a + b
func(6, 7)
我认为像 %%timeit
这样的魔术函数会导致笔记本解释单元格。 运行 你在命令行和笔记本中的代码,我得到的结果与你的相似。
但是,如果我在一个单元格中定义函数 afunc
和 bfunc
,然后在不同的单元格中分别定义函数 %%timeit
,结果将再次相互一致。
[1]
def afunc(a: int, b: int) -> int:
return a + b
def bfunc(a, b):
return a + b
[2]
%%timeit
afunc(1,2)
40.9 ns ± 0.131 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
[3]
%%timeit
bfunc(1,2)
42.9 ns ± 0.0417 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
无显着差异,而
[1]
%%timeit
def afunc(a: int, b: int) -> int:
return a + b
afunc(1,2)
145 ns ± 0.765 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
[2]
%%timeit
def bfunc(a, b):
return a + b
bfunc(1,2)
71.2 ns ± 0.341 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
给我们带来了巨大的不同
我们可以更进一步,这样做:
[1]
%%timeit
def afunc(a: int, b: int) -> int:
return a + b
111 ns ± 0.358 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
[2]
%%timeit
def bfunc(a, b):
return a + b
32.6 ns ± 0.255 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
这里很明显,解释是正在测量的内容,因为没有执行用户函数调用,只是函数本身的定义。我们观察到相同的差异,减去 ~40ns 到 运行 任一函数。
控制台中更简单的代码似乎证实这不是 JupyterLab 特有的,尽管此代码更简单以满足 exec
的要求:
C:\Users\Ben>python -m timeit "exec('a: int = 1')"
50000 loops, best of 5: 4.91 usec per loop
C:\Users\Ben>python -m timeit "exec('a = 1')"
50000 loops, best of 5: 4.02 usec per loop
正如您自己注意到的,“函数定义需要更多时间”。
为什么你看到了不同
Pythengers,反汇编!
import dis
dis.dis('''
def func(a, b):
return a + b
''')
输出(使用Python 3.10.4):
2 0 LOAD_CONST 0 (<code object func at 0x00000243938989D0, file "<dis>", line 2>)
2 LOAD_CONST 1 ('func')
4 MAKE_FUNCTION 0
6 STORE_NAME 0 (func)
8 LOAD_CONST 2 (None)
10 RETURN_VALUE
Disassembly of <code object func at 0x00000243938989D0, file "<dis>", line 2>:
3 0 LOAD_FAST 0 (a)
2 LOAD_FAST 1 (b)
4 BINARY_ADD
6 RETURN_VALUE
加载已编译的代码对象,从中创建一个函数对象,并将其分配给名称 func
。
使用带注释的版本,我们得到的是:
2 0 LOAD_CONST 0 ('a')
2 LOAD_NAME 0 (int)
4 LOAD_CONST 1 ('b')
6 LOAD_NAME 0 (int)
8 LOAD_CONST 2 ('return')
10 LOAD_NAME 0 (int)
12 BUILD_TUPLE 6
14 LOAD_CONST 3 (<code object func at 0x00000243940F0A80, file "<dis>", line 2>)
16 LOAD_CONST 4 ('func')
18 MAKE_FUNCTION 4 (annotations)
20 STORE_NAME 1 (func)
22 LOAD_CONST 5 (None)
24 RETURN_VALUE
Disassembly of <code object func at 0x00000243940F0A80, file "<dis>", line 2>:
3 0 LOAD_FAST 0 (a)
2 LOAD_FAST 1 (b)
4 BINARY_ADD
6 RETURN_VALUE
因此,当 that 被执行时,需要为注释完成一些额外的工作,这会花费额外的时间。
为什么你的同学没有看出区别
为什么你同学的测量方式没有区别?因为他们并没有真正测量它。尝试在 a.py
:
打印一些东西
print('importing a.py ...')
def func(a, b):
return a + b
func(6, 7)
然后再试一次:
> python -m timeit -n 10000000 "import a"
importing a.py ...
10000000 loops, best of 5: 370 nsec per loop
>
一千万个循环,但我们的消息只打印了一次。因为 Python 缓存导入并且不会重新导入已经导入的内容。因此,尽管此 确实 衡量了函数定义的 单次 执行,但在执行一千万个 import 语句时,这完全无关紧要。实际上,您的同学并没有测量他们打算测量的代码,而是测量了 导入语句 (几乎所有这些都被识别为重新导入然后被忽略)。
我想测试类型提示是否会影响代码执行到 运行 的时间。它可能会增加一点点时间,因为编译器必须忽略它,这需要时间,但我想看看这是多么微不足道。
为此,我在 jupyterLab 上执行了这个:
然而,我的一个同学在没有 jupyterLab 的情况下尝试了这个,发现了这个:
有人能解释为什么会这样吗?
使用的函数:
def func(a, b):
return a + b
func(6, 7)
和:
def func(a: int, b: int) -> int:
return a + b
func(6, 7)
我认为像 %%timeit
这样的魔术函数会导致笔记本解释单元格。 运行 你在命令行和笔记本中的代码,我得到的结果与你的相似。
但是,如果我在一个单元格中定义函数 afunc
和 bfunc
,然后在不同的单元格中分别定义函数 %%timeit
,结果将再次相互一致。
[1]
def afunc(a: int, b: int) -> int:
return a + b
def bfunc(a, b):
return a + b
[2]
%%timeit
afunc(1,2)
40.9 ns ± 0.131 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
[3]
%%timeit
bfunc(1,2)
42.9 ns ± 0.0417 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
无显着差异,而
[1]
%%timeit
def afunc(a: int, b: int) -> int:
return a + b
afunc(1,2)
145 ns ± 0.765 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
[2]
%%timeit
def bfunc(a, b):
return a + b
bfunc(1,2)
71.2 ns ± 0.341 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
给我们带来了巨大的不同
我们可以更进一步,这样做:
[1]
%%timeit
def afunc(a: int, b: int) -> int:
return a + b
111 ns ± 0.358 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
[2]
%%timeit
def bfunc(a, b):
return a + b
32.6 ns ± 0.255 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
这里很明显,解释是正在测量的内容,因为没有执行用户函数调用,只是函数本身的定义。我们观察到相同的差异,减去 ~40ns 到 运行 任一函数。
控制台中更简单的代码似乎证实这不是 JupyterLab 特有的,尽管此代码更简单以满足 exec
的要求:
C:\Users\Ben>python -m timeit "exec('a: int = 1')"
50000 loops, best of 5: 4.91 usec per loop
C:\Users\Ben>python -m timeit "exec('a = 1')"
50000 loops, best of 5: 4.02 usec per loop
正如您自己注意到的,“函数定义需要更多时间”。
为什么你看到了不同
Pythengers,反汇编!
import dis
dis.dis('''
def func(a, b):
return a + b
''')
输出(使用Python 3.10.4):
2 0 LOAD_CONST 0 (<code object func at 0x00000243938989D0, file "<dis>", line 2>)
2 LOAD_CONST 1 ('func')
4 MAKE_FUNCTION 0
6 STORE_NAME 0 (func)
8 LOAD_CONST 2 (None)
10 RETURN_VALUE
Disassembly of <code object func at 0x00000243938989D0, file "<dis>", line 2>:
3 0 LOAD_FAST 0 (a)
2 LOAD_FAST 1 (b)
4 BINARY_ADD
6 RETURN_VALUE
加载已编译的代码对象,从中创建一个函数对象,并将其分配给名称 func
。
使用带注释的版本,我们得到的是:
2 0 LOAD_CONST 0 ('a')
2 LOAD_NAME 0 (int)
4 LOAD_CONST 1 ('b')
6 LOAD_NAME 0 (int)
8 LOAD_CONST 2 ('return')
10 LOAD_NAME 0 (int)
12 BUILD_TUPLE 6
14 LOAD_CONST 3 (<code object func at 0x00000243940F0A80, file "<dis>", line 2>)
16 LOAD_CONST 4 ('func')
18 MAKE_FUNCTION 4 (annotations)
20 STORE_NAME 1 (func)
22 LOAD_CONST 5 (None)
24 RETURN_VALUE
Disassembly of <code object func at 0x00000243940F0A80, file "<dis>", line 2>:
3 0 LOAD_FAST 0 (a)
2 LOAD_FAST 1 (b)
4 BINARY_ADD
6 RETURN_VALUE
因此,当 that 被执行时,需要为注释完成一些额外的工作,这会花费额外的时间。
为什么你的同学没有看出区别
为什么你同学的测量方式没有区别?因为他们并没有真正测量它。尝试在 a.py
:
print('importing a.py ...')
def func(a, b):
return a + b
func(6, 7)
然后再试一次:
> python -m timeit -n 10000000 "import a"
importing a.py ...
10000000 loops, best of 5: 370 nsec per loop
>
一千万个循环,但我们的消息只打印了一次。因为 Python 缓存导入并且不会重新导入已经导入的内容。因此,尽管此 确实 衡量了函数定义的 单次 执行,但在执行一千万个 import 语句时,这完全无关紧要。实际上,您的同学并没有测量他们打算测量的代码,而是测量了 导入语句 (几乎所有这些都被识别为重新导入然后被忽略)。