VGo*_*nPa 8 python profiling function-calls python-internals
在这篇文章中,Guido van Rossum说功能调用可能很昂贵,但我不明白为什么也不贵.
多少延迟会为您的代码添加一个简单的函数调用,为什么?
Mar*_*ers 20
函数调用要求暂停当前执行帧,并在堆栈上创建并推送新帧.与许多其他操作相比,这相对昂贵.
您可以测量timeit模块所需的确切时间:
>>> import timeit
>>> def f(): pass
...
>>> timeit.timeit(f)
0.15175890922546387
Run Code Online (Sandbox Code Playgroud)
这是百万次调用空函数的1/6秒; 你要比较你所想要的功能所需的时间; 如果性能是个问题,则需要考虑0.15秒.
kfs*_*one 12
Python 有一个“相对较高”的函数调用开销,这是我们为 Python 的一些最有用的功能付出的代价。
猴子补丁:
你在 Python 中拥有强大的猴子补丁/覆盖行为的能力,以至于解释器无法保证给定的
a, b = X(1), X(2)
return a.fn() + b.fn() + a.fn()
Run Code Online (Sandbox Code Playgroud)
a.fn() 和 b.fn() 是一样的,或者 a.fn() 在 b.fn() 被调用后会是一样的。
In [1]: def f(a, b):
...: return a.fn() + b.fn() + c.fn()
...:
In [2]: dis.dis(f)
1 0 LOAD_FAST 0 (a)
3 LOAD_ATTR 0 (fn)
6 CALL_FUNCTION 0
9 LOAD_FAST 1 (b)
12 LOAD_ATTR 0 (fn)
15 CALL_FUNCTION 0
18 BINARY_ADD
19 LOAD_GLOBAL 1 (c)
22 LOAD_ATTR 0 (fn)
25 CALL_FUNCTION 0
28 BINARY_ADD
29 RETURN_VALUE
Run Code Online (Sandbox Code Playgroud)
在上面,您可以看到在每个位置都查找了 'fn'。这同样适用于变量,但人们似乎更清楚这一点。
In [11]: def g(a):
...: return a.i + a.i + a.i
...:
In [12]: dis.dis(g)
2 0 LOAD_FAST 0 (a)
3 LOAD_ATTR 0 (i)
6 LOAD_FAST 0 (a)
9 LOAD_ATTR 0 (i)
12 BINARY_ADD
13 LOAD_FAST 0 (a)
16 LOAD_ATTR 0 (i)
19 BINARY_ADD
20 RETURN_VALUE
Run Code Online (Sandbox Code Playgroud)
更糟糕的是,因为模块可以修补/替换自己/彼此,如果您正在调用全局/模块函数,则每次都必须查找全局/模块:
In [16]: def h():
...: v = numpy.vector(numpy.vector.identity)
...: for i in range(100):
...: v = numpy.vector.add(v, numpy.vector.identity)
...:
In [17]: dis.dis(h)
2 0 LOAD_GLOBAL 0 (numpy)
3 LOAD_ATTR 1 (vector)
6 LOAD_GLOBAL 0 (numpy)
9 LOAD_ATTR 1 (vector)
12 LOAD_ATTR 2 (identity)
15 CALL_FUNCTION 1
18 STORE_FAST 0 (v)
3 21 SETUP_LOOP 47 (to 71)
24 LOAD_GLOBAL 3 (range)
27 LOAD_CONST 1 (100)
30 CALL_FUNCTION 1
33 GET_ITER
>> 34 FOR_ITER 33 (to 70)
37 STORE_FAST 1 (i)
4 40 LOAD_GLOBAL 0 (numpy)
43 LOAD_ATTR 1 (vector)
46 LOAD_ATTR 4 (add)
49 LOAD_FAST 0 (v)
52 LOAD_GLOBAL 0 (numpy)
55 LOAD_ATTR 1 (vector)
58 LOAD_ATTR 2 (identity)
61 CALL_FUNCTION 2
64 STORE_FAST 0 (v)
67 JUMP_ABSOLUTE 34
>> 70 POP_BLOCK
>> 71 LOAD_CONST 0 (None)
74 RETURN_VALUE
Run Code Online (Sandbox Code Playgroud)
变通方法
考虑捕获或导入您希望不会发生变化的任何值:
def f1(files):
for filename in files:
if os.path.exists(filename):
yield filename
# vs
def f2(files):
from os.path import exists
for filename in files:
if exists(filename):
yield filename
# or
def f3(files, exists=os.path.exists):
for filename in files:
if exists(filename):
yield filename
Run Code Online (Sandbox Code Playgroud)
另请参阅“野外”部分
但是,并不总是可以导入;例如,你可以导入 sys.stdin 但不能导入 sys.stdin.readline,numpy 类型也会有类似的问题:
In [15]: def h():
...: from numpy import vector
...: add = vector.add
...: idy = vector.identity
...: v = vector(idy)
...: for i in range(100):
...: v = add(v, idy)
...:
In [16]: dis.dis(h)
2 0 LOAD_CONST 1 (-1)
3 LOAD_CONST 2 (('vector',))
6 IMPORT_NAME 0 (numpy)
9 IMPORT_FROM 1 (vector)
12 STORE_FAST 0 (vector)
15 POP_TOP
3 16 LOAD_FAST 0 (vector)
19 LOAD_ATTR 2 (add)
22 STORE_FAST 1 (add)
4 25 LOAD_FAST 0 (vector)
28 LOAD_ATTR 3 (identity)
31 STORE_FAST 2 (idy)
5 34 LOAD_FAST 0 (vector)
37 LOAD_FAST 2 (idy)
40 CALL_FUNCTION 1
43 STORE_FAST 3 (v)
6 46 SETUP_LOOP 35 (to 84)
49 LOAD_GLOBAL 4 (range)
52 LOAD_CONST 3 (100)
55 CALL_FUNCTION 1
58 GET_ITER
>> 59 FOR_ITER 21 (to 83)
62 STORE_FAST 4 (i)
7 65 LOAD_FAST 1 (add)
68 LOAD_FAST 3 (v)
71 LOAD_FAST 2 (idy)
74 CALL_FUNCTION 2
77 STORE_FAST 3 (v)
80 JUMP_ABSOLUTE 59
>> 83 POP_BLOCK
>> 84 LOAD_CONST 0 (None)
87 RETURN_VALUE
Run Code Online (Sandbox Code Playgroud)
CAVEAT EMPTOR: - 捕获变量不是零成本操作,它会增加帧大小, - 仅在识别热代码路径后使用,
参数传递
Python 的参数传递机制看起来微不足道,但与大多数语言不同,它的成本很高。我们正在谈论将参数分成 args 和 kwargs:
f(1, 2, 3)
f(1, 2, c=3)
f(c=3)
f(1, 2) # c is auto-injected
Run Code Online (Sandbox Code Playgroud)
在 CALL_FUNCTION 操作中有很多工作要做,包括可能从 C 层到 Python 层再返回的一个转换。
除此之外,经常需要查找参数才能传递:
f(obj.x, obj.y, obj.z)
Run Code Online (Sandbox Code Playgroud)
考虑:
In [28]: def fn(obj):
...: f = some.module.function
...: for x in range(1000):
...: for y in range(1000):
...: f(x + obj.x, y + obj.y, obj.z)
...:
In [29]: dis.dis(fn)
2 0 LOAD_GLOBAL 0 (some)
3 LOAD_ATTR 1 (module)
6 LOAD_ATTR 2 (function)
9 STORE_FAST 1 (f)
3 12 SETUP_LOOP 76 (to 91)
15 LOAD_GLOBAL 3 (range)
18 LOAD_CONST 1 (1000)
21 CALL_FUNCTION 1
24 GET_ITER
>> 25 FOR_ITER 62 (to 90)
28 STORE_FAST 2 (x)
4 31 SETUP_LOOP 53 (to 87)
34 LOAD_GLOBAL 3 (range)
37 LOAD_CONST 1 (1000)
40 CALL_FUNCTION 1
43 GET_ITER
>> 44 FOR_ITER 39 (to 86)
47 STORE_FAST 3 (y)
5 50 LOAD_FAST 1 (f)
53 LOAD_FAST 2 (x)
56 LOAD_FAST 0 (obj)
59 LOAD_ATTR 4 (x)
62 BINARY_ADD
63 LOAD_FAST 3 (y)
66 LOAD_FAST 0 (obj)
69 LOAD_ATTR 5 (y)
72 BINARY_ADD
73 LOAD_FAST 0 (obj)
76 LOAD_ATTR 6 (z)
79 CALL_FUNCTION 3
82 POP_TOP
83 JUMP_ABSOLUTE 44
>> 86 POP_BLOCK
>> 87 JUMP_ABSOLUTE 25
>> 90 POP_BLOCK
>> 91 LOAD_CONST 0 (None)
94 RETURN_VALUE
Run Code Online (Sandbox Code Playgroud)
其中“LOAD_GLOBAL”要求对名称进行散列,然后在全局表中查询该散列值。这是一个 O(log N) 操作。
但是想想看:对于我们的两个简单的 0-1000 循环,我们做了一百万次......
LOAD_FAST 和 LOAD_ATTR 也是哈希表查找,它们只限于特定的哈希表。LOAD_FAST 查询 locals() 哈希表,LOAD_ATTR 查询最后加载的对象的哈希表...
但还要注意,我们在那里调用了一个函数一百万次。幸运的是,它是一个内置函数,内置函数的开销要小得多;但如果这确实是您的性能热点,您可能需要考虑通过执行以下操作来优化范围开销:
x, y = 0, 0
for i in range(1000 * 1000):
....
y += 1
if y > 1000:
x, y = x + 1, 0
Run Code Online (Sandbox Code Playgroud)
您可以对捕获变量进行一些黑客攻击,但它可能对这段代码的性能影响最小,并且只会降低其可维护性。
但是这个问题的核心pythonic修复是使用生成器或迭代器:
for i in obj.values():
prepare(i)
# vs
prepare(obj.values())
Run Code Online (Sandbox Code Playgroud)
和
for i in ("left", "right", "up", "down"):
test_move(i)
# vs
test_move(("left", "right", "up", "down"))
Run Code Online (Sandbox Code Playgroud)
和
for x in range(-1000, 1000):
for y in range(-1000, 1000):
fn(x + obj.x, y + obj.y, obj.z)
# vs
def coordinates(obj):
for x in range(obj.x - 1000, obj.x + 1000 + 1):
for y in range(obj.y - 1000, obj.y + 1000 + 1):
yield obj.x, obj.y, obj.z
fn(coordinates(obj))
Run Code Online (Sandbox Code Playgroud)
在野外
你会在野外以这样的形式看到这些 pythopticisms:
def some_fn(a, b, c, stdin=sys.stdin):
...
Run Code Online (Sandbox Code Playgroud)
这有几个优点:
大多数 numpy 调用要么接受要么具有接受列表、数组等的变体,如果您不使用这些,您可能会错过 numpy 99% 的好处。
def distances(target, candidates):
values = []
for candidate in candidates:
values.append(numpy.linalg.norm(candidate - target))
return numpy.array(values)
# vs
def distances(target, candidates):
return numpy.linalg.norm(candidates - target)
Run Code Online (Sandbox Code Playgroud)
(注意:这不一定是获得距离的最佳方法,尤其是如果您不打算将距离值转发到其他地方;例如,如果您正在进行范围检查,使用更具选择性的方法可能更有效,以避免使用 sqrt 操作)
优化可迭代对象不仅意味着传递它们,还意味着返回它们
def f4(files, exists=os.path.exists):
return (filename for filename in files if exists(filename))
^- returns a generator expression
Run Code Online (Sandbox Code Playgroud)
任何形式"X都很昂贵"的陈述都没有考虑到性能总是与其他任何事情相关,而且相对于其他任务而言,任务都可以完成.
有很多关于SO的问题表达了对可能存在但通常不存在性能问题的关注.
至于函数调用是否昂贵,通常有两部分答案.
对于功能很少而且不需要调用其他子功能的功能,以及在特定应用中占用总时钟时间的10%以上的功能,值得尝试内嵌它们或以其他方式降低成本调用.
在包含复杂数据结构和/或高抽象层次结构的应用程序中,函数调用是昂贵的,不是因为它们花费的时间,而是因为它们诱使您制作更多它们而不是严格必要的.当这种情况发生在多个抽象层次上时,效率低下就会成倍增加,产生一种不易局部化的复合减速.
生成有效代码的方法是后验,而不是先验.首先编写代码,使其干净且可维护,包括您喜欢的函数调用.然后,当它以实际工作负载运行时,让它告诉您可以采取哪些措施来加快速度. 这是一个例子.