メソッドや関数に機能を抽出することは、特にOOPにおいて、コードのモジュール性、可読性、相互運用性にとって不可欠です。
しかし、これはより多くの関数呼び出しが行われることを意味します。
コードをメソッドまたは関数に分割すると、実際にmodern *言語のパフォーマンスにどのような影響がありますか?
*最も人気のあるもの:C、Java、C++、C#、Python、JavaScript、Ruby ...
多分。コンパイラーは、「この関数は数回しか呼び出されず、速度を最適化することになっているので、この関数をインライン化する」と判断するかもしれません。基本的に、コンパイラーは関数呼び出しを関数の本体で置き換えます。たとえば、ソースコードは次のようになります。
void DoSomething()
{
a = a + 1;
DoSomethingElse(a);
}
void DoSomethingElse(int a)
{
b = a + 3;
}
コンパイラはDoSomethingElse
をインライン化することを決定し、コードは次のようになります。
void DoSomething()
{
a = a + 1;
b = a + 3;
}
関数がインライン化されていない場合、はい、関数呼び出しを行うとパフォーマンスが低下します。ただし、非常に小さなヒットであるため、非常に高性能なコードのみが関数呼び出しを心配することになります。これらの種類のプロジェクトでは、コードは通常アセンブリで記述されます。
関数呼び出し(プラットフォームによって異なります)には通常、数十の命令が含まれます。これには、スタックの保存/復元が含まれます。一部の関数呼び出しは、ジャンプと戻りの命令で構成されています。
しかし、関数呼び出しのパフォーマンスに影響を与える可能性のある他のことがいくつかあります。呼び出されている関数がプロセッサのキャッシュに読み込まれず、キャッシュミスが発生し、メモリコントローラーがメインRAMから関数を取得する必要があります。これはパフォーマンスに大きな影響を与える可能性があります。
簡単に言えば、関数呼び出しはパフォーマンスに影響する場合とそうでない場合があります。伝える唯一の方法は、コードをプロファイルすることです。遅いコードスポットがどこにあるかを推測しようとしないでください。コンパイラとハードウェアには、信じられないほどのトリックがいくつかあります。コードをプロファイリングして、スロースポットの場所を取得します。
これはコンパイラーまたはランタイム(およびそのオプション)の実装の問題であり、確実に言うことはできません。
CおよびC++内では、一部のコンパイラーは最適化設定に基づいて呼び出しをインライン化します-これは、 などのツールを見たときに生成されたアセンブリを調べることで簡単に確認できますhttps://gcc.godbolt.org/
Javaなどのその他の言語では、これはランタイムの一部として使用されます。これはJITの一部であり、 this SO =質問 。特に、HotSpotの JVMオプションを見てください
-XX:InlineSmallCode=n
生成されたネイティブコードのサイズがこれよりも小さい場合にのみ、以前にコンパイルされたメソッドをインライン化します。デフォルト値は、JVMが実行されているプラットフォームによって異なります。-XX:MaxInlineSize=35
インライン化されるメソッドの最大バイトコードサイズ。-XX:FreqInlineSize=n
インライン化される、頻繁に実行されるメソッドの最大バイトコードサイズ。デフォルト値は、JVMが実行されているプラットフォームによって異なります。
したがって、はい、HotSpot JITコンパイラーは、特定の基準を満たすメソッドをインライン化します。
これの影響は、各JVM(またはコンパイラ)が異なる動作をする可能性があり、言語の広い範囲で答えようとするため、判別が困難です。ほとんど間違い間違い。影響は、適切な実行環境でコードをプロファイリングし、コンパイルされた出力を調べることによってのみ適切に判断できます。
これは、CPythonがインライン化されておらず、一部の呼び出しがインライン化されているJython(JVMで実行されているPython)による誤ったアプローチと見なすことができます。同様に、MRI Ruby JRubyはインライン化しませんが、Ruby2cはCへのRubyへのトランスパイラーです...でコンパイルされたCコンパイラオプション。
言語はインライン化されません。実装may。
あなたは間違った場所でパフォーマンスを探しています。関数呼び出しの問題は、それらに多くのコストがかかることではありません。別の問題があります。関数呼び出しは完全に無料である可能性があり、それでもこの別の問題が発生します。
機能がクレジットカードのようなものです。簡単に使えるので、多めに使う傾向があります。必要以上に20%多いとしましょう。次に、典型的な大規模なソフトウェアにはいくつかの層が含まれ、それぞれが下の層の関数を呼び出します。そのため、1.2の係数は層の数によって悪化する可能性があります。 (たとえば、5つのレイヤーがあり、各レイヤーのスローダウンファクターが1.2である場合、複合スローダウンファクターは1.2 ^ 5または2.5です。)これはそれについて考える1つの方法にすぎません。
これは、関数呼び出しを避ける必要があるという意味ではありません。つまり、コードが稼働しているときは、無駄を見つけて排除する方法を知っておく必要があります。 stackexchangeサイトでこれを行う方法については、非常に優れたアドバイスがあります。 This は私の貢献の1つを与えます。
追加:小さな例。かつて私は、一連の作業指示または「ジョブ」を追跡する工場フロアのソフトウェアのチームで働いていました。ジョブが完了したかどうかを通知できる関数JobDone(idJob)
がありました。サブタスクがすべて完了したときにジョブが完了し、サブ操作がすべて完了したときにジョブが完了しました。これらすべては、リレーショナルデータベースで追跡されていました。別の関数を1回呼び出すだけですべての情報を抽出できるため、JobDone
がその別の関数を呼び出し、ジョブが完了したかどうかを確認して、残りを破棄しました。そうすれば、人々はこのようなコードを簡単に書くことができます:
while(!JobDone(idJob)){
...
}
または
foreach(idJob in jobs){
if (JobDone(idJob)){
...
}
}
ポイントがわかりますか?この関数は非常に「強力」であり、簡単に呼び出すことができたため、呼び出されすぎました。したがって、パフォーマンスの問題は、関数に出入りする命令ではありませんでした。それは、仕事が完了したかどうかを知るためのより直接的な方法が必要であるということでした。繰り返しになりますが、このコードは、何千行もある無害なコードに埋め込まれている可能性があります。事前に修正しようとすることは誰もがしようとすることですが、それは暗い部屋でダーツを投げようとするようなものです。代わりに必要なのは、それを実行することであり、その後時間をかけるだけで、「スローコード」にそれが何であるかを通知させます。そのために random pausing を使用します。
それは本当に言語と機能に依存すると思います。 cおよびc ++コンパイラーは多くの関数をインライン化できますが、PythonまたはJavaの場合はそうではありません。
Javaの具体的な詳細はわかりませんが(すべてのメソッドは仮想ですが、ドキュメントをよく確認することをお勧めします)、Python私はインライン化がないこと、末尾再帰の最適化、関数呼び出しが非常に高価であることを確認してください。
Python関数は基本的に実行可能なオブジェクトです(そして、実際にはcall()メソッドを定義してオブジェクトインスタンスを関数にすることもできます)。これは、それらを呼び出すのにかなり多くのオーバーヘッドがあることを意味します...
だが
関数内で変数を定義すると、インタープリターはバイトコードで通常のLOAD命令の代わりにLOADFASTを使用して、コードを高速化します...
もう1つは、呼び出し可能オブジェクトを定義すると、メモ化などのパターンが可能になり、メモリを多く使用する代わりに計算を大幅に高速化できることです。基本的に、それは常にトレードオフです。関数呼び出しのコストは、スタックに実際にコピーする必要があるものを決定するため、パラメーターにも依存します(したがって、c/c ++では、構造体のような大きなパラメーターを値ではなくポインター/参照で渡すのが一般的です)。
あなたの質問は実際には広すぎて、stackexchangeで完全に答えることはできないと思います。
私が行うことをお勧めするのは、1つの言語から始めて、その特定の言語によって関数呼び出しがどのように実装されるかを理解するために高度なドキュメントを研究することです。
このプロセスでどれだけ多くのことを学べるかに驚くでしょう。
特定の問題がある場合は、測定/プロファイリングを行い、天気を判断して、関数を作成するか、同等のコードをコピーして貼り付けることをお勧めします。
もっと具体的な質問をすれば、もっと具体的な答えが得られると思います。
しばらく前にXenon PowerPCで直接および仮想C++関数呼び出しのオーバーヘッドを測定しました 。
問題の関数には単一のパラメーターと単一の戻り値があったため、パラメーターの受け渡しはレジスターで発生しました。
要するに、直接(非仮想)関数呼び出しのオーバーヘッドは、インライン関数呼び出しと比較して、約5.5ナノ秒、つまり18クロックサイクルでした。仮想関数呼び出しのオーバーヘッドは、インラインと比較して13.2ナノ秒、つまり42クロックサイクルでした。
これらのタイミングは、プロセッサファミリによって異なる可能性があります。 私のテストコードはこちら ;ハードウェアで同じ実験を実行できます。 CFastTimerの実装には、 rdtsc のような高精度タイマーを使用します。システムのtime()は、十分に正確ではありません。