为什么Python中的函数/方法调用昂贵?

问题描述 投票:8回答:3

this post,吉多·范罗苏姆说,一个函数调用可能是昂贵的,但我不明白为什么,也没有多少昂贵的都可以。

多少延迟添加到您的代码简单的函数调用,为什么?

python profiling function-calls python-internals
3个回答
20
投票

函数调用要求当前帧执行被暂停,并创建一个新的帧和压入堆栈。这是相对昂贵的,相对于许多其他操作。

您可以测量与timeit模块所需的确切时间:

>>> import timeit
>>> def f(): pass
... 
>>> timeit.timeit(f)
0.15175890922546387

这是第二个一百万的呼叫转移至空函数的1/6;你会比较无论你是想投入功能所需的时间; 0.15第二种则需要考虑到,如果性能是一个问题。


7
投票

形式为“X为贵”的任何语句不考虑到性能总是相对于其他任何的事情,而相对于其他但是任务可以完成。

有上,这样表达的事情,可能是问题,但通常不是很多问题,性能问题。

至于函数调用是否是昂贵的,有一个一般的两部分答案。

  1. 对于确实很少,不叫进一步的子功能,并且在特定的应用程序是负责总挂钟时间超过10%的功能,这是值得一试的在线它们或以其他方式降低成本调用。
  2. 在包含复杂的数据结构和/或高的抽象层次的应用,函数调用是昂贵的,不是因为他们走的时候,而是因为他们引诱你做比严格意义上更多的人。当这种情况发生在多个抽象层次,低效率相乘在一起,产生一个复合的减速是不容易局部化。

以产生高效的代码的方法是后验,而不是先验。先写代码,所以它是清洁和维护,包括函数调用,只要你喜欢。然后,它与实际工作量运行时,让它告诉你什么可以做,以加快速度。 Here's an example.


3
投票

Python有一个"relatively high"函数调用的开销,这是我们对一些Python的最有用的功能支付费用。

猴子修补:

你有这么大的权力,以猴补丁/忽略行为在Python的解释不能保证给

 a, b = X(1), X(2)
 return a.fn() + b.fn() + a.fn()

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

在上面,你可以看到,“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

更糟的是,因为模块可以猴补丁/替换自己/对方,如果你调用一个全局/模块功能,全球/模块每次要查找:

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

替代方法

考虑拍摄或导入你期望不发生变异的任何值:

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

又见“在野外”节

这并不总是可能的进口,虽然;例如,您可以导入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

买者自负: - 捕获变量不是零成本操作,并且它增加了帧的大小, - 仅识别热代码路径后使用,


参数传递

Python的参数传递机制看起来微不足道,但不像大多数语言它花费了很多。我们谈论的是分离参数到指定参数和kwargs:

f(1, 2, 3)
f(1, 2, c=3)
f(c=3)
f(1, 2)  # c is auto-injected

还有大量的工作是继续在CALL_FUNCTION操作,包括可能从C层到Python的层和后面一个转变。

除此之外,参数经常需要查找传递:

f(obj.x, obj.y, obj.z)

考虑:

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

其中,“LOAD_GLOBAL”要求名字被散列,然后全局表中查询该散列值。这是一个为O​​(log N)操作。

不过想想这一点:我们两国,简单,0-1000循环,我们正在做的一百万次......

LOAD_FAST和LOAD_ATTR也是哈希表的查找,他们只是局限在特定的哈希表。 LOAD_FAST咨询当地人()哈希表,LOAD_ATTR咨询最后加载对象的哈希表...

还要注意,我们调用一个函数有一百万次。幸运的是,这是一个内置的功能,并内建有一个更昂贵的开销;但如果这是真的你PERF热点,你可能要考虑做类似优化范围的开销:

x, y = 0, 0
for i in range(1000 * 1000):
    ....
    y += 1
    if y > 1000:
        x, y = x + 1, 0

你可以做捕获变量的一些黑客攻击,但它可能对这个代码最小PERF影响,只是使其不太维护。

但核心Python的修复这个问题是使用发电机或iterables:

for i in obj.values():
    prepare(i)

# vs

prepare(obj.values())

for i in ("left", "right", "up", "down"):
    test_move(i)

# vs

test_move(("left", "right", "up", "down"))

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))

在野外

你会看到在这样的形式野生这些pythopticisms:

def some_fn(a, b, c, stdin=sys.stdin):
    ...

这有几个优点:

  • 影响帮助()这个函数,(默认输入标准输入)
  • 提供了一个钩单元测试,
  • 促进sys.stdin到本地(LOAD_FAST VS LOAD_GLOBAL + LOAD_ATTR)

大多数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)

(注:这是不是一定要得到距离的最好方式,尤其如果你不打算在其他地方转发的距离值;例如,如果你正在做的范围检查,它可能是更有效的使用,避免了更选择性的方法使用SQRT操作)

优化iterables并不仅仅意味着将它们传递,而且他们回国

def f4(files, exists=os.path.exists):
    return (filename for filename in files if exists(filename))
           ^- returns a generator expression
© www.soinside.com 2019 - 2024. All rights reserved.