Pythonは、戻り値としてのみ使用される変数を最適化しますか?


106

次の2つのコードスニペット間に最終的な違いはありますか?1つ目は、関数内の変数に値を割り当て、その変数を返します。2番目の関数は、値を直接返すだけです。

Pythonはそれらを同等のバイトコードに変換しますか?それらの1つはより高速ですか?

ケース1

def func():
    a = 42
    return a

ケース2

def func():
    return 42

5
dis.dis(..)両方で使用すると、違いがあることわかります。そうです。しかし、ほとんどの実際のアプリケーションでは、関数の処理の遅延と比較した場合のオーバーヘッドはそれほど大きくありません。
Willem Van Onsem 2017

4
2つの可能性があります。(a)この関数をタイトなループで何度も(つまり、少なくとも100万回)呼び出します。その場合、Python関数を呼び出す必要はまったくありませんが、代わりにnumpyライブラリなどを使用してループをベクトル化する必要があります。(b)この関数を何度も呼び出すことはありません。その場合、これらの機能の速度の違いはあまり心配する価値がありません。
アーサータッカ2017

回答:


138

いいえ、ありません

CPythonバイトコードへのコンパイルは、基本的な最適化のみを行うように設計された小さなのぞき穴オプティマイザーのみを通過します(これらの最適化の詳細については、テストスイートのtest_peepholer.pyを参照してください)。

実際に何が発生するかdisを確認するには、*を使用して生成された命令を確認します。割り当てを含む最初の関数の場合:

from dis import dis
dis(func)
  2           0 LOAD_CONST               1 (42)
              2 STORE_FAST               0 (a)

  3           4 LOAD_FAST                0 (a)
              6 RETURN_VALUE

一方、2番目の関数の場合:

dis(func2)
  2           0 LOAD_CONST               1 (42)
              2 RETURN_VALUE

最初の2つの(高速)命令が使用されます:STORE_FASTLOAD_FAST。これらfastlocalsは、現在の実行フレームの配列に値をすばやく格納して取得します。次に、どちらの場合でも、a RETURN_VALUEが実行されます。したがって、2番目のコマンドは、実行に必要なコマンドが少ないため、わずかに速くなります。

一般に、CPythonコンパイラーは、実行する最適化において保守的であることに注意してください。これは、他のコンパイラ(一般に、処理する情報がはるかに多いコンパイラ)ほど賢くはありません。明らかに正しいことは別として、主な設計目標は、a)シンプルに保つこと、およびb)これらをコンパイルする際にできるだけ迅速にして、コンパイルフェーズが存在することに気付かないようにすることです。

結局、このような小さな問題に悩まされるべきではありません。速度の利点は小さく、一定であり、Pythonが解釈されるという事実によってもたらされるオーバーヘッドによって小さくなります。

* disは、コードを逆アセンブルする小さなPythonモジュールです。これを使用して、VMが実行するPythonバイトコードを確認できます。

注: @Jorn Verneeのコメントでも述べられているように、これはPythonのCPython実装に固有のものです。他の実装では、必要に応じてより積極的な最適化を行う場合がありますが、CPythonは行いません。


11
Pythonの人(c ++)ではないので、それが内部でどのように機能するかはわかりませんが、最初のケースが2番目のケースに最適化されるべきではありませんか?まともなC ++コンパイラは、その最適化を行います。
NathanOliver 2017

7
@NathanOliverそれは実際にはそうではありません、Pythonはスマートにプレイすることさえせずに、ここで述べられているように行います。
Dimitris Fasarakis Hilliard 2017

80
@NathanOliverのこの質問への回答の完全に合理的でインテリジェントな推測が完全に間違っているという事実は、私の目には、これが「自明」、「ナンセンス」、「愚か」な質問ではないことの証明ですTigerhawkT3が私たちに信じさせるように、「考える時間をとる」ことによって。何年もプロのPythonプログラマであったにもかかわらず、私が答えを確信できなかったのは、有効で興味深い質問です。
マークアメリー2017

Pythonのコンパイラはせいぜい「保守的」であり、「非常に保守的」ではありません。主な設計目標は、「できるだけ迅速に...コンパイルフェーズが存在することに気付かないこと」ではありません。それは、「シンプルに保つ」後の二次的なものです。「1 <<(2 ** 34)」や「b'x '*(2 ** 32)」などの大きな定数を持つ関数は、コンパイルに数秒かかり、GBサイズの定数が生成されます。実行します。大きな文字列はコンパイラーによっても破棄されます。これらのケースに対して提案された修正は、コンパイラーが複雑すぎるため拒否されました。
Andrew Dalke 2017年

@AndrewDalkeインサイダーがこれについてコメントしてくれたことに感謝します。私はあなたが指摘した問題に対処するために文言を微調整しました。
Dimitris Fasarakis Hilliard

3

最初のケースでは、オブジェクト42が単に名前付き変数に割り当てられていること、aつまり、名前(ie a)が値(ie 42)を参照していることを除いて、どちらも基本的に同じです。データをコピーしないという意味で、技術的には割り当てを行いません。

処理中、最初のケースではreturnこの名前付きバインディングaが返さ42れ、2番目のケースではオブジェクトが返されます。

詳細については、Ned Batchelderによるこの素晴らしい記事を参照してください。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.