関数呼び出しはパフォーマンスにどの程度影響しますか?


13

メソッドや関数に機能を抽出することは、特にOOPにおいて、コードのモジュール性、可読性、相互運用性にとって不可欠です。

しかし、これはより多くの関数呼び出しが行われることを意味します。

コードをメソッドまたは関数に分割すると、現代の*言語のパフォーマンスに実際にどのよう影響がありますか?

*最も人気のあるもの:C、Java、C ++、C#、Python、JavaScript、Ruby ...



1
その塩の価値があるすべての言語実装は、数十年前からインライン化を行っていると思います。IOW、オーバーヘッドは正確に0です。
JörgW Mittag

1
これらの呼び出しの多くは、コードを処理するさまざまなコンパイラー/インタープリターとインライン化するものによってオーバーヘッドが最適化されるため、「より多くの関数呼び出しが行われる」ことは多くの場合当てはまりません。あなたの言語がこれらの種類の最適化を持っていなければ、私はそれを現代のものとは思わないかもしれません。
Ixrec

2
パフォーマンスにどのように影響しますか?使用する特定の言語、実際のコードの構造、および使用しているコンパイラーのバージョン、そして場合によっては使用しているプラ​​ットフォームに応じて、処理速度が速くなるか、遅くなるか、または変更されません。再実行。あなたが得るすべての答えは、この不確実性のいくつかのバリエーションであり、より多くの言葉とより多くの裏付けとなる証拠があります。
GrandOpener 2016年

1
インパクトは、もしあれば、あなた、人は、決してほど小さい今までそれに気づきません。心配するべき他のはるかに重要なことがある。タブを5スペースにするか7スペースにするかと同様です。
MetaFight 2016年

回答:


21

多分。コンパイラーは「この関数は数回しか呼び出されず、速度を最適化することになっているので、この関数をインライン化する」と判断するかもしれません。基本的に、コンパイラーは関数呼び出しを関数の本体で置き換えます。たとえば、ソースコードは次のようになります。

void DoSomething()
{
   a = a + 1;
   DoSomethingElse(a);
}

void DoSomethingElse(int a)
{
   b = a + 3;
}

コンパイラはインライン化することを決定しDoSomethingElse、コードは

void DoSomething()
{
   a = a + 1;
   b = a + 3;
}

関数がインライン化されていない場合、はい、関数呼び出しを行うとパフォーマンスが低下します。ただし、非常に小さなヒットであるため、非常に高性能なコードだけが関数呼び出しを心配することになります。これらの種類のプロジェクトでは、コードは通常、アセンブリで記述されます。

関数呼び出し(プラットフォームによって異なります)には通常、数十の命令が含まれます。これには、スタックの保存/復元が含まれます。一部の関数呼び出しは、ジャンプと戻りの命令で構成されています。

しかし、関数呼び出しのパフォーマンスに影響を与える可能性のある他のことがいくつかあります。呼び出されている関数がプロセッサのキャッシュに読み込まれず、キャッシュミスが発生し、メモリコントローラーがメインRAMから関数を取得する必要があります。これはパフォーマンスに大きな影響を与える可能性があります。

簡単に言えば、関数呼び出しはパフォーマンスに影響する場合とそうでない場合があります。伝える唯一の方法は、コードをプロファイルすることです。遅いコードスポットがどこにあるかを推測しようとしないでください。コンパイラとハードウェアには、信じられないほどのトリックがいくつかあります。コードをプロファイリングして、スロースポットの場所を取得します。


1
私が最近のコンパイラー(gcc、clang)を使用して、大規模な関数内のループに対して非常に悪いコードを作成することを本当に気にかけた状況で見ました。ループを静的関数に抽出しても、インライン化されたため、役に立ちませんでした。一部のケースで作成された外部関数にループを抽出すると、速度が大幅に(ベンチマークで測定可能)向上します。
gnasher729 2016年

1
私はこれに便乗し、OPは時期尚早の最適化に
Patrick

1
@パトリックビンゴ。最適化する場合は、プロファイラーを使用して、遅いセクションがどこにあるかを確認します。推測しないでください。通常、遅いセクションがどこにあるかを感じることができますが、プロファイラーで確認してください。
CHendrix 2016年

@ gnasher729特定の問題を解決するには、プロファイラー以上のものが必要になります-逆アセンブルされたマシンコードの読み方も学ぶ必要があります。時期尚早な最適化はありますが、時期尚早な学習などはありません(少なくともソフトウェア開発では)。
rwong 2016年

あなたは可能性があなたが関数に百万回を呼び出している場合は、この問題を持っていますが、かなり大きな影響力を持っている他の問題を持っている可能性があります。
マイケルショー

5

これはコンパイラーまたはランタイム(およびそのオプション)の実装の問題であり、確実に言うことはできません。

CおよびC ++内で、一部のコンパイラーは最適化設定に基づいて呼び出しをインライン化します-これは、https://gcc.godbolt.org/などのツールを見たときに生成されたアセンブリを調べると簡単に確認できます

Javaなどの他の言語では、これがランタイムの一部として含まれています。これはJITの一部であり、このSOの質問で詳しく説明されています。特にHotSpotのJVMオプションを見てください。

-XX:InlineSmallCode=n 生成されたネイティブコードサイズがこれよりも小さい場合にのみ、以前にコンパイルされたメソッドをインライン化します。デフォルト値は、JVMが実行されているプラ​​ットフォームによって異なります。
-XX:MaxInlineSize=35 インライン化されるメソッドの最大バイトコードサイズ。
-XX:FreqInlineSize=n インライン化される、頻繁に実行されるメソッドの最大バイトコードサイズ。デフォルト値は、JVMが実行されているプラ​​ットフォームによって異なります。

したがって、はい、HotSpot JITコンパイラは特定の条件を満たすメソッドをインライン化します。

この影響は、JVM(またはコンパイラ)ごとに異なる動作をする可能性があり、言語の広い範囲で答えようとすることはほぼ間違いなく間違っているため、判別するのは困難です。影響は、適切な実行環境でコードをプロファイリングし、コンパイルされた出力を調べることによってのみ適切に判断できます。

これは、CPythonがインライン化されておらず、Jython(JVMで実行されているPython)がいくつかの呼び出しをインライン化している、誤ったアプローチと見なすことができます。同様に、JRubyがインライン化しないMRI Rubyと、RubyをCにトランスパイラーするruby2cは、コンパイルされたCコンパイラオプションに応じてインライン化されるかどうかに関係なく、

言語はインライン化されません。実装はしてもよいです


5

あなたは間違った場所でパフォーマンスを探しています。関数呼び出しの問題は、それらに多くのコストがかかることではありません。別の問題があります。関数呼び出しは完全に無料である可能性があり、それでもこの別の問題が発生します。

機能がクレジットカードのようなものです。簡単に使えるので、多めに使う傾向があります。必要以上に20%多いとしましょう。次に、典型的な大規模なソフトウェアにはいくつかの層が含まれ、それぞれが下の層の関数を呼び出すため、1.2の係数は層の数によって悪化する可能性があります。(たとえば、5つのレイヤーがあり、各レイヤーのスローダウン係数が1.2である場合、複合スローダウン係数は1.2 ^ 5または2.5です。)これはそれについて考える1つの方法にすぎません。

これは、関数呼び出しを避ける必要があるという意味ではありません。つまり、コードが稼働しているときは、無駄を見つけて排除する方法を知っている必要があります。stackexchangeサイトでこれを行う方法については、非常に優れたアドバイスがあります。 これは私の貢献の1つを与えます。

追加:小さな例。かつて私は、一連の作業指示または「ジョブ」を追跡する工場フロアのソフトウェアのチームで働いていました。JobDone(idJob)ジョブが完了したかどうかを通知できる関数がありました。サブタスクがすべて完了するとジョブが完了し、サブ操作がすべて完了するとそれぞれが完了します。これらすべては、リレーショナルデータベースで追跡されていました。別の関数への1回の呼び出しでJobDone、その他の関数と呼ばれるすべての情報を抽出し、ジョブが完了したかどうかを確認し、残りを破棄しました。そうすれば、人々はこのようなコードを簡単に書くことができます:

while(!JobDone(idJob)){
    ...
}

または

foreach(idJob in jobs){
    if (JobDone(idJob)){
        ...
    }
}

ポイントがわかりますか?この関数は非常に「強力」であり、簡単に呼び出すことができたため、呼び出されすぎました。したがって、パフォーマンスの問題は、関数に出入りする命令ではありませんでした。それは、仕事が完了したかどうかを知るためのより直接的な方法が必要であるということでした。繰り返しになりますが、このコードは、何千行もある無害なコードに埋め込まれている可能性があります。事前に修正しようとすることは誰もがしようとすることですが、それは暗い部屋でダーツを投げようとするようなものです。あなたが代わりに必要なのは、それが実行してもらう、とすることで、その後「スローコードは」単に時間をかけて、それが何であるかを教えてみましょう。そのために私はランダムな一時停止を使用します。


1

それは本当に言語と機能に依存すると思います。cおよびc ++コンパイラーは多くの関数をインライン化できますが、これはPythonまたはJavaには当てはまりません。

Javaの具体的な詳細はわかりませんが(すべてのメソッドは仮想ですが、ドキュメントをよく確認することをお勧めします)、Pythonではインライン化がなく、末尾再帰の最適化や関数の呼び出しにはかなりの費用がかかります。

Python関数は基本的に実行可能なオブジェクトです(実際には、call()メソッドを定義してオブジェクトインスタンスを関数にすることもできます)。これは、それらを呼び出すのにかなりのオーバーヘッドがあることを意味します...

だが

関数内で変数を定義すると、インタープリターはバイトコードで通常のLOAD命令の代わりにLOADFASTを使用し、コードを高速化します...

もう1つは、呼び出し可能オブジェクトを定義すると、メモ化などのパターンが可能になり、(より多くのメモリを使用する代わりに)計算を大幅に高速化できることです。基本的に、それは常にトレードオフです。関数呼び出しのコストは、スタックに実際にコピーする必要があるものを決定するため、パラメーターにも依存します(したがって、c / c ++では、構造体などの大きなパラメーターを、値ではなくポインター/参照で渡すのが一般的です)。

あなたの質問は実際には広すぎて、stackexchangeで完全に答えることはできないと思います。

私が行うことをお勧めするのは、1つの言語から始めて、その特定の言語によって関数呼び出しがどのように実装されるかを理解するために高度なドキュメントを研究することです。

このプロセスでどれだけ多くのことを学べるかに驚くでしょう。

特定の問題がある場合は、測定/プロファイリングを行い、天気を判断して、関数を作成するか、同等のコードをコピーして貼り付けることをお勧めします。

もっと具体的な質問をすれば、もっと具体的な答えが得られると思います。


引用:「あなたの質問は実際には広範すぎて、stackexchangeで完全に回答するには不十分だと思います。」どうすればそれを絞り込むことができますか?パフォーマンスにおける関数呼び出しの影響を表す実際のデータをいくつか見てみたいと思います。どんな言語でもかまいませんが、私が言ったように、可能であればデータでバックアップされた、より詳細な説明を知りたいだけです。
dabadaba 2016年

重要なのは、それは言語に依存するということです。CおよびC ++では、関数がインライン化されている場合、影響は0です。インライン化されていない場合、パラメーターに依存します(キャッシュにあるかどうかなど)
ingframin

1

しばらく前に、Xenon PowerPCで直接および仮想C ++関数呼び出しのオーバーヘッドを測定しました

問題の関数は単一のパラメーターと単一の戻り値を持っていたため、パラメーターの受け渡しはレジスターで発生しました。

要するに、直接(非仮想)関数呼び出しのオーバーヘッドは、インライン関数呼び出しと比較して、約5.5ナノ秒、つまり18クロックサイクルでした。仮想関数呼び出しのオーバーヘッドは、インラインと比較して13.2ナノ秒、つまり42クロックサイクルでした。

これらのタイミングは、プロセッサフ​​ァミリによって異なる可能性があります。私のテストコードはここにあります。ハードウェアで同じ実験を実行できます。CFastTimerの実装には、rdtscのような高精度タイマーを使用します。システムのtime()は十分に正確ではありません。

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