メソッドや関数に機能を抽出することは、特にOOPにおいて、コードのモジュール性、可読性、相互運用性にとって不可欠です。
しかし、これはより多くの関数呼び出しが行われることを意味します。
コードをメソッドまたは関数に分割すると、現代の*言語のパフォーマンスに実際にどのような影響がありますか?
*最も人気のあるもの:C、Java、C ++、C#、Python、JavaScript、Ruby ...
メソッドや関数に機能を抽出することは、特にOOPにおいて、コードのモジュール性、可読性、相互運用性にとって不可欠です。
しかし、これはより多くの関数呼び出しが行われることを意味します。
コードをメソッドまたは関数に分割すると、現代の*言語のパフォーマンスに実際にどのような影響がありますか?
*最も人気のあるもの: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の一部であり、この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コンパイラオプションに応じてインライン化されるかどうかに関係なく、
言語はインライン化されません。実装はしてもよいです。
あなたは間違った場所でパフォーマンスを探しています。関数呼び出しの問題は、それらに多くのコストがかかることではありません。別の問題があります。関数呼び出しは完全に無料である可能性があり、それでもこの別の問題が発生します。
機能がクレジットカードのようなものです。簡単に使えるので、多めに使う傾向があります。必要以上に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)){
...
}
}
ポイントがわかりますか?この関数は非常に「強力」であり、簡単に呼び出すことができたため、呼び出されすぎました。したがって、パフォーマンスの問題は、関数に出入りする命令ではありませんでした。それは、仕事が完了したかどうかを知るためのより直接的な方法が必要であるということでした。繰り返しになりますが、このコードは、何千行もある無害なコードに埋め込まれている可能性があります。事前に修正しようとすることは誰もがしようとすることですが、それは暗い部屋でダーツを投げようとするようなものです。あなたが代わりに必要なのは、それが実行してもらう、とすることで、その後「スローコードは」単に時間をかけて、それが何であるかを教えてみましょう。そのために私はランダムな一時停止を使用します。
それは本当に言語と機能に依存すると思います。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()は十分に正確ではありません。