10

This is mostly to make sure my methodology is correct, but my basic question was is it worth it to check outside of a function if I need to access the function at all. I know, I know, premature optimization, but in many cases, its the difference between putting an if statement inside the function call to determine whether I need to run the rest of the code, or putting it before the function call. In other words, it takes no effort to do it one way or the other. Right now, all the checks are mixed between both, and I'd like the get it all nice and standardized.

The main reason I asked is because the other answers I saw mostly referenced timeit, but that gave me negative numbers, so I switched to this:

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

and got this for output

   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)

To me that shows that not calling the function is .31 seconds, and calling it takes 1.52 seconds, which is almost 5 times slower. But like I said, I got negative numbers with timeit, so I want to make sure its actually that slow.

Also from what I gather, the reason function calls are so slow is because python needs to look up to make sure the function still exists before it can run it or something? Isn't there any way to just tell it to like...assume that everything is still there so that it doesn't have to do unnecessary work that (apparently) slows it down 5x?

4

1 に答える 1

42

ここではリンゴとナシを比較しています。1 つのメソッドは単純な割り当てを行い、もう 1 つのメソッドは関数を呼び出します。はい、関数呼び出しによってオーバーヘッドが追加されます。

次の場合は、これを最小限に抑える必要がありtimeitます。

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

これで、関数呼び出しを追加fooしただけで (同じことを行います)、関数呼び出しにかかる余分な時間を測定できます。これが約 4 倍遅いとは言えません。いいえ、関数呼び出しにより、 1.000.000回の反復で 0.11 秒のオーバーヘッドが追加されます。

代わりに、a = 5100 万回の反復を実行するのに 0.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関数呼び出しから戻り、再びスタックの一番上にあるものを戻り値として受け取ります。

于 2013-02-01T14:33:43.410 に答える