Python函数调用速度非常慢

11

这主要是为了确保我的方法正确,但我的基本问题是,如果我需要访问函数,是否值得在函数外部检查。我知道,我知道,过早优化,但在许多情况下,这是放置if语句以确定是否需要运行其余代码的函数调用内部和函数调用前面之间的区别。换句话说,将其一种方式或另一种方式实现都不需要太多的工作。现在,所有的检查都混杂在两者之间,我想将它们全部标准化。

我提出这个问题的主要原因是因为我看到的其他答案大多都涉及timeit,但那给了我负数,所以我转而采用了这种方式:

import timeit
import cProfile

def aaaa(idd):
    return idd

def main():
    #start = timeit.timeit()
    for i in range(9999999):
        a = 5
    #end = timeit.timeit()
    #print("1", end - start)

def main2():
    #start = timeit.timeit()
    for i in range(9999999):
        aaaa(5)
    #end = timeit.timeit()
    #print("2", end - start)

cProfile.run('main()', sort='cumulative')
cProfile.run('main2()', sort='cumulative')

并且得到了这个输出

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    0.310    0.310 {built-in method exec}
        1    0.000    0.000    0.310    0.310 <string>:1(<module>)
        1    0.310    0.310    0.310    0.310 test.py:7(main)
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    2.044    2.044 {built-in method exec}
        1    0.000    0.000    2.044    2.044 <string>:1(<module>)
        1    1.522    1.522    2.044    2.044 test.py:14(main2)
  9999999    0.521    0.000    0.521    0.000 test.py:4(aaaa)

对我来说,这表明不调用该函数需要0.31秒,而调用它需要1.52秒,几乎慢了5倍。但是正如我所说,使用timeit时我得到了负数,因此我想确保它实际上就是这么慢。

另外据我所知,函数调用之所以如此缓慢是因为Python需要查找以确保函数仍然存在,然后才能运行它或者做其他一些事情?难道没有办法告诉Python假设所有东西都还在,这样它就不必执行不必要的工作,从而提高速度(显然可以)?


2
你在这里比较苹果和梨。你正在比较将赋值给本地变量和调用函数,这是两件非常不同的事情。 - Martijn Pieters
1
你是如何调用timeit函数得到负数的? - Martijn Pieters
1
我认为5倍慢实际上是快的。我的意思是,调用函数比简单赋值要复杂得多。你必须1)查找函数是否存在(这是一个全局查找,很慢),2)打包参数,3)调用函数本身必须解包参数,4)执行代码,5)返回对象。(我使用5步骤只是偶然。我不认为这与代码变慢5倍有任何关系) - Bakuriu
调用 timeit.timeit() 是测量执行一百万次无操作所需的时间。从 timeit.timeit() 中减去 timeit.timeit() 可能是负数。 - unutbu
Martijn: 那就是重点(如果我没有做错的话),比较在函数内部执行与在函数外部执行某些操作。我想知道函数调用本身增加的开销是什么。我注释掉了我的 timeit 方法来展示我正在做什么。 - DanielCardin
顺便问一下:相对于什么慢?C、Java?显然,在Python中,您不能假装将函数调用绑定到编译时的函数,因为在运行时可以重新分配它们。与编译语言相比,很难获得可比较的时间,因为编译语言在编译时执行此操作,失去了在运行时重新定义事物的能力。此外,如果函数执行任何最小化的工作,则调用的开销相对于计算中总时间的花费来说是绝对可以忽略不计的。 - Bakuriu
1个回答

43

您在这里比较苹果和梨。一个方法只是简单赋值,另一个调用函数。是的,函数调用会增加开销。

您应该将其削减到timeit的最低限度:

>>> import timeit
>>> timeit.timeit('a = 5')
0.03456282615661621
>>> timeit.timeit('foo()', 'def foo(): a = 5')
0.14389896392822266

现在我们所做的只是添加一个函数调用(foo执行相同的操作),因此您可以测量函数调用需要的额外时间。您不能说这慢了近4倍,不,函数调用添加了0.11秒的超时时间,对于1,000,000次迭代。

如果我们使用需要执行一百万次迭代花费0.5秒钟的其他内容代替a = 5,将它们移动到一个函数中不会使事情花费2秒钟。现在需要0.61秒,因为函数开销不会增加。

函数调用需要操作堆栈,将本地框架推入其中,创建新框架,然后在函数返回时清除所有内容。

换句话说,将语句移动到一个函数中会增加一些开销,您移动到该函数中的语句越多,开销所占比例就越小。函数从不使这些语句变慢。

Python函数只是存储在变量中的对象;您可以将函数分配给不同的变量,用完全不同的东西替换它们,或随时删除它们。当您调用函数时,首先引用存储它们的名称(foo),然后调用函数对象((arguments));在动态语言中,这个查找必须每次都发生。

您可以在函数生成的字节码中看到这一点:

>>> def foo():
...     pass
... 
>>> def bar():
...     return foo()
... 
>>> import dis
>>> dis.dis(bar)
  2           0 LOAD_GLOBAL              0 (foo)
              3 CALL_FUNCTION            0
              6 RETURN_VALUE        

LOAD_GLOBAL 操作码在全局命名空间中查找名称(foo)(基本上是哈希表查找),并将结果推送到堆栈上。 CALL_FUNCTION 然后调用堆栈中的任何内容,并用返回值替换它。 RETURN_VALUE 从函数调用返回,再次将堆栈上最顶部的内容作为返回值。


尽管它可能会使它们看起来变慢,例如在将新值分配给现有变量与在函数范围内创建新变量并反复对其进行赋值之间进行比较的情况下。是的,在底层它们并不做同样的事情,但从对代码的天真分析来看,它们看起来是相同的。 - Silas Ray
1
堆栈的操作等在所有语言中都会发生,不是吗?无论您使用哪种语言,这些成本都会发生,对吧?我更多地谈到了我对Python需要做额外工作的印象,因为Python是动态的,函数可以被创建和消失。这导致我后来提出了一个问题,即是否可以强制Python假定它仍然存在,而不必每次都查找它(实际上我对此并不了解,请告知我是否在胡说八道!) - DanielCardin
3
函数也是存储在变量中的对象;每次访问其名称时都会查找它们。你无法解决这个问题,这是动态语言的本质。 - Martijn Pieters

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