私は宗教家であり、罪を犯さないように努めています。だから、私は -===-)よりも小さい(それよりも小さい、Robert C. Martinを言い換える)関数を- -(クリーンコード 聖書。しかし、いくつかのものをチェックしている間に、私は this post に到達しました。その下にこのコメントを読みました:
言語によっては、メソッド呼び出しのコストが高額になる可能性があることに注意してください。ほとんどの場合、読み取り可能なコードの記述とパフォーマンスの高いコードの記述の間にはトレードオフがあります。
パフォーマンスの高い最新のコンパイラーの豊富な業界を考えると、この引用されたステートメントは現在どのような条件下でまだ有効ですか?
それが私の唯一の質問です。そして、それは私が長いか小さい関数を書くべきかどうかについてではありません。私はあなたのフィードバックが私の態度を変えることに寄与するかもしれないし、そうでないかもしれないことを強調し、 冒涜者 の誘惑に抵抗することができないままにします。
ドメインによって異なります。
低消費電力のマイクロコントローラー用のコードを記述している場合、メソッド呼び出しのコストは非常に大きくなる可能性があります。ただし、通常のWebサイトまたはアプリケーションを作成している場合、メソッド呼び出しのコストは、残りのコードと比較してごくわずかです。その場合、メソッド呼び出しのようなマイクロ最適化ではなく、常に正しいアルゴリズムとデータ構造に焦点を当てる価値があります。
また、コンパイラーがメソッドをインライン化するという問題もあります。ほとんどのコンパイラは、可能な場合は関数をインライン化できるほどインテリジェントです。
そして最後に、パフォーマンスの黄金律があります。常に最初にプロファイルします。仮定に基づいて「最適化された」コードを記述しないでください。よくわからない場合は、両方のケースを書き、どちらが良いかを確認してください。
関数呼び出しのオーバーヘッドは、完全に言語、および最適化するレベルによって異なります。
超低レベルでは、関数呼び出し、さらには仮想メソッド呼び出しは、分岐の予測ミスやCPUキャッシュミスにつながると、コストがかかる可能性があります。 assembler と記述した場合は、呼び出しの前後でレジスタを保存および復元するためにいくつかの追加の指示が必要であることもわかります。コンパイラーは言語のセマンティクス(特に、インターフェースメソッドディスパッチや動的に読み込まれるライブラリーなどの機能)によって制限されるため、「十分にスマート」なコンパイラーが正しい関数をインライン化してこのオーバーヘッドを回避できるとは限りません。
高レベルでは、Perl、Pythonなどの言語Rubyは関数呼び出しごとに多くの簿記を行うため、比較的コストがかかります。これはメタプログラミングによってさらに悪化します。私は一度= Pythonソフトウェアは、関数呼び出しを非常にホットなループから引き上げるだけで3倍になります。パフォーマンスが重要なコードでは、ヘルパー関数をインライン化することで顕著な効果が得られます。
しかし、ソフトウェアの大部分は、関数呼び出しのオーバーヘッドに気付くことができるほどパフォーマンスがそれほど重要ではありません。 いずれにしても、クリーンでシンプルなコードを書くことは報われます:
コードがパフォーマンスを重視しない場合、これによりメンテナンスが容易になります。パフォーマンスが重要なソフトウェアであっても、コードの大部分は「ホットスポット」にはなりません。
コードがパフォーマンスを重視する場合、単純なコードを使用すると、コードを理解し、最適化の機会を見つけることが容易になります。最大の成果は通常、関数のインライン化などのマイクロ最適化ではなく、アルゴリズムの改善によるものです。または別の言い方をすると、同じことを速くしないでください。より少ないことをする方法を見つけなさい。
「単純なコード」は「千の小さな関数に分解された」という意味ではないことに注意してください。すべての関数はまた、認知的なオーバーヘッドを少し導入します–より抽象的なコードについて reason を行うのはより困難です。ある時点で、これらの小さな関数はほとんど機能せず、それらを使用しないとコードが単純になる可能性があります。
パフォーマンスのためにコードを調整することに関するほとんどすべての格言は、 アムダールの法則 の特別なケースです。アムダールの法則の短くユーモラスな声明は、
プログラムの一部が実行時間の5%を占め、その部分を最適化してzero実行時間の割合がかかるようにすると、プログラム全体としては5%だけ速くなります。
(ランタイムの0%まで物事を最適化することは完全に可能です。大規模で複雑なプログラムを最適化するために座っているとき、ランタイムの少なくとも一部をものに費やしていることに気付く可能性が高いですそれは必要ありませんまったくすること。)
これが、人々が通常、関数呼び出しのコストを気にしないように言う理由です。たとえどれほど高くても、通常プログラム全体は、ランタイムのほんの一部を呼び出しオーバーヘッドに費やしているだけなので、高速化されますアップはあまり役に立ちません。
しかし、あなたが引っ張ることができるトリックがあればall関数呼び出しが速くなりますが、そのトリックはおそらく価値があります。コンパイラの開発者は、関数の「プロローグ」と「エピローグ」を最適化するために多くの時間を費やしています。なぜなら、それがそれぞれのほんの少しのビットであっても、すべてのプログラムがそのコンパイラでコンパイルされるからです。
そして、もしあなたがプログラムisが関数呼び出しを行うだけでそのランタイムの多くを費やしていると信じる理由があるなら、それらの関数呼び出しのいくつかが不要かどうか考え始める必要があります。これをいつ行うべきかを知るための経験則を次に示します。
関数の呼び出しごとのランタイムが1ミリ秒未満であるにもかかわらず、その関数が数十万回呼び出されている場合は、おそらくインライン化する必要があります。
プログラムのプロファイルに数千の関数が表示され、それらのnoneが実行時間の0.1%を超える場合、関数呼び出しのオーバーヘッドはおそらく合計で重要です。
「 lasagna code 」があり、次のレイヤーへのディスパッチ以外の作業をほとんど行わない抽象化のレイヤーが多数あり、これらのレイヤーのすべてが仮想メソッド呼び出しで実装されている場合、 CPUが間接分岐パイプラインのストールで多くの時間を浪費している可能性が高いです。残念ながら、これを解決する唯一の方法は、いくつかの層を取り除くことです。
私はこの引用に挑戦します:
ほとんどの場合、読み取り可能なコードの記述とパフォーマンスの高いコードの記述の間にはトレードオフがあります。
これは本当に誤解を招く表現であり、潜在的に危険な態度です。トレードオフを行う必要がある特定のケースがいくつかありますが、一般的に2つの要素は独立しています。
必要なトレードオフの例は、単純なアルゴリズムと、より複雑であるがより高性能なアルゴリズムがある場合です。ハッシュテーブルの実装は、リンクリストの実装よりも明らかに複雑ですが、ルックアップが遅くなるため、パフォーマンスとシンプルさ(読みやすさの要因の1つ)のトレードオフが必要になる場合があります。
関数呼び出しのオーバーヘッドに関して、再帰アルゴリズムを反復に変えることは、アルゴリズムと言語によっては大きなメリットがあるかもしれません。しかし、これは非常に具体的なシナリオであり、一般に関数呼び出しのオーバーヘッドは無視できるか最適化されます。
(Pythonのような一部の動的言語には、メソッド呼び出しのオーバーヘッドがかなりあります。ただし、パフォーマンスが問題になる場合は、最初のPythonを使用しないでください。場所。)
読み取り可能なコードのほとんどの原則-一貫したフォーマット、意味のある識別子名、適切で役立つコメントなどは、パフォーマンスに影響を与えません。また、文字列ではなく列挙型を使用するなど、パフォーマンス上の利点もあります。
パフォーマンスがプログラムにとって重要であり、実際にプログラムに多数の呼び出しがある場合、呼び出しの種類によっては、コストが問題になる場合と問題にならない場合があります。
呼び出された関数が小さく、コンパイラーがそれをインライン化できる場合、コストは本質的にゼロになります。最新のコンパイラ/言語実装には、JIT、リンク時最適化、および/または有益なときに関数をインライン化する機能を最大化するように設計されたモジュールシステムがあります。
OTOH、関数呼び出しには明白なコストがあります。それらが存在するだけで、呼び出しの前後でコンパイラの最適化が妨げられる可能性があります。
呼び出された関数が何を行うか(コンパイラーが仮想/動的ディスパッチまたは動的ライブラリーの関数など)を推論できない場合、その関数は副作用を持っている可能性があると悲観的に想定する必要があります。例外をスローして変更します。グローバル状態、またはポインタを通して見られるすべてのメモリを変更します。コンパイラは一時的な値を保存してメモリをバックアップし、呼び出し後にそれらを再度読み取る必要がある場合があります。呼び出しに関連する命令を並べ替えることはできないため、ループをベクトル化したり、冗長な計算をループから引き上げたりすることができない場合があります。
たとえば、ループの各反復で不要に関数を呼び出す場合:
for(int i=0; i < /* gasp! */ strlen(s); i++) x ^= s[i];
コンパイラーはそれが純粋な関数であることを認識し、ループの外に移動する可能性があります(この例のようなひどい場合には、偶発的なO(n ^ 2)アルゴリズムもO(n)に修正されます)。
for(int i=0, end=strlen(s); i < end; i++) x ^= s[i];
そして、ワイド/ SIMD命令を使用して一度に4/8/16要素を処理するようにループを書き換えることもできます。
ただし、ループ内の不透明なコードへの呼び出しを追加すると、呼び出しが何もせず、それ自体が非常に安価であっても、コンパイラーは最悪の事態を想定する必要があります—呼び出しは、s
と同じメモリーを指すグローバル変数にアクセスしますその内容を変更します(関数内でconst
であっても、他の場所ではconst
でなくてもかまいません)。最適化を不可能にします。
for(int i=0; i < strlen(s); i++) {
x ^= s[i];
do_nothing();
}
ほとんどの場合、関数呼び出しのオーバーヘッドは重要ではありません。
ただし、コードのインライン化による大きなメリットは、インライン化後の新しいコードの最適化です。
たとえば、定数引数を使用して関数を呼び出す場合、オプティマイザーは、呼び出しをインライン化する前に、その引数を定数引数で折り畳むことができませんでした。引数が関数ポインター(またはラムダ)の場合、オプティマイザーはそのラムダへの呼び出しもインライン化できるようになりました。
これは、実際の関数ポインターが呼び出しサイトまでずっと折り畳まれていない限り、仮想関数と関数ポインターをインライン化できないため、魅力的ではない大きな理由です。
C++では、引数をコピーする関数呼び出しの設計に注意してください。デフォルトは「値渡し」です。レジスタやその他のスタックフレーム関連のものを保存することによる関数呼び出しのオーバーヘッドは、オブジェクトの意図しない(そして潜在的に非常に高価な)コピーによって圧倒される可能性があります。
高度に因数分解されたコードをあきらめる前に調査する必要があるスタックフレーム関連の最適化があります。
遅いプログラムに対処しなければならなかったほとんどの場合、アルゴリズムの変更を行うと、インライン関数呼び出しよりもはるかに高速になることがわかりました。たとえば、別のエンジニアがmap-of-maps構造を埋めるパーサーを再実行しました。その一環として、キャッシュされたインデックスを1つのマップから論理的に関連付けられたマップに削除しました。これはニースコードの堅牢性の動きでしたが、格納されたインデックスを使用するのではなく、将来のすべてのアクセスに対してハッシュルックアップを実行するために100倍の速度低下のため、プログラムが使用できなくなりました。プロファイリングは、ほとんどの時間がハッシュ関数に費やされていることを示しました。
これ 古い紙 はあなたの質問に答えるかもしれません:
Guy Lewis Steele、Jr ..「「高価なプロシージャコール」の神話、または有害と見なされるプロシージャコールの実装、またはラムダを暴く:究極のGOTO」。 MIT AI Lab。AI Lab MemoAIM-443。1977年10月。
概要:
フォークロアは、GOTOステートメントは「安価」であるが、プロシージャコールは「高価」であると述べています。この神話は、主に設計が不十分な言語実装の結果です。この神話の歴史的な成長が考慮されます。この神話を覆す理論的アイデアと既存の実装の両方が議論されています。プロシージャコールを無制限に使用すると、スタイリッシュで自由な操作が可能になります。特に、追加の変数を導入することなく、フローチャートを「構造化」プログラムとして作成できます。 GOTOステートメントとプロシージャコールの難しさは、抽象プログラミングの概念と具体的な言語構造との矛盾として特徴付けられます。
はい、ブランチの予測を逃すことは、数十年前よりも現代のハードウェアの方がコストがかかりますが、コンパイラーはこれを最適化することではるかに賢くなりました。
例として、Javaを考えます。一見すると、この言語では関数呼び出しのオーバーヘッドが特に支配的であるはずです。
これらの慣行に恐怖を感じ、平均的なCプログラマーは、JavaはCよりも1桁以上遅いはずであると予測します。そして20年前は彼は正しかったでしょう。しかし、現代のベンチマークはJava同等のCコードの数パーセント以内のコード。それはどのようにして可能ですか?
1つの理由は、当然のことながら、最近のJVMは関数呼び出しをインライン化しているためです。それは投機的なインライン化を使用してそうします:
つまり、コード:
int x = point.getX();
に書き直される
if (point.class != Point) GOTO interpreter;
x = point.x;
そしてもちろん、ランタイムは、ポイントが割り当てられていない限りこのタイプチェックを上に移動するのに十分スマートです。または、呼び出し元のコードがタイプを認識している場合はそれを省略します。
要約すると、Javaが自動メソッドのインライン化を管理する場合でも、コンパイラーが自動インライン化をサポートできなかった固有の理由はありません。インライン化は最新のプロセッサーで非常に有益であるためです。したがって、この最も基本的な最適化戦略を知らない現代の主流のコンパイラーを想像することはほとんどできず、別の方法で証明されない限り、これが可能なコンパイラーであると推定します。
他の人が言うように、最初にプログラムのパフォーマンスを測定する必要があり、おそらく実際に違いはありません。
それでも、概念的なレベルから、私はあなたの質問で混同されているいくつかのことを明確にすると思いました。まず、あなたは尋ねます:
最近のコンパイラーでは、関数呼び出しのコストは依然として重要ですか?
キーワード「関数」と「コンパイラ」に注意してください。あなたの見積もりは微妙に異なります:
言語によっては、メソッド呼び出しのコストが高額になる可能性があることに注意してください。
これは、オブジェクト指向の意味でのメソッドについて話しています。
「関数」と「メソッド」はしばしば交換可能に使用されますが、コスト(あなたが尋ねている)とコンパイル(あなたが与えたコンテキスト)に関しては違いがあります。
特に、静的ディスパッチと動的ディスパッチについて知る必要があります。今のところ最適化は無視します。
Cのような言語では、通常functionsをstatic dispatchで呼び出します。例えば:
_int foo(int x) {
return x + 1;
}
int bar(int y) {
return foo(y);
}
int main() {
return bar(42);
}
_
コンパイラーがfoo(y)
の呼び出しを検出すると、foo
名が参照している関数がわかるため、出力プログラムはfoo
関数に直接ジャンプできます。これは非常に安価です。それがstatic dispatchの意味です。
代替手段は動的ディスパッチで、コンパイラは関数が呼び出されています。例として、ここにいくつかのHaskellコードがあります(Cの同等物は乱雑になるためです)。
_foo x = x + 1
bar f x = f x
main = print (bar foo 42)
_
ここで、bar
関数は、引数f
を呼び出しています。これは何でもかまいません。したがって、どこにジャンプするかわからないため、コンパイラーはbar
を高速ジャンプ命令にコンパイルすることはできません。代わりに、bar
用に生成するコードは、f
を逆参照して、どの関数が指しているのかを調べてから、その関数にジャンプします。それがdynamic dispatchの意味です。
これらの例は両方ともfunctions用です。 methodsについて説明しましたが、これは動的にディスパッチされる関数の特定のスタイルと考えることができます。たとえば、Pythonは次のとおりです。
_class A:
def __init__(self, x):
self.x = x
def foo(self):
return self.x + 1
def bar(y):
return y.foo()
z = A(42)
bar(z)
_
y.foo()
呼び出しは動的ディスパッチを使用します。これは、foo
オブジェクトのy
プロパティの値を検索し、見つかったものを呼び出すためです。 y
がクラスA
を持つこと、またはA
クラスにfoo
メソッドが含まれていることを知らないため、すぐにジャンプすることはできません。
OK、それが基本的な考え方です。静的ディスパッチは、コンパイルまたは解釈するかどうかに関係なく、動的ディスパッチに関係なく速いことに注意してください。他のすべてが等しい。逆参照では、どちらの方法でも追加のコストが発生します。
それでは、これは現代の最適化コンパイラにどのように影響しますか?
最初に注意すべきことは、静的ディスパッチをより大きく最適化できることです。ジャンプ先の関数がわかっている場合は、インライン化などを実行できます。動的ディスパッチでは、実行時までジャンプすることが分からないため、実行できる最適化はあまりありません。
第2に、一部の言語では、一部の動的ディスパッチがジャンプを終了するinferが可能なため、静的ディスパッチに最適化できます。これにより、インライン化などの他の最適化を実行できます。
上記のPythonの例では、Pythonによって他のコードがクラスとプロパティをオーバーライドできるため、このような推論は絶望的です。すべての場合。
私たちの言語で、たとえば注釈を使用してy
をクラスA
に制限するなどの制限を課すことができる場合、その情報を使用してターゲット関数を推測できます。 y
は実際には異なる(サブ)クラスを持っている可能性があるため、サブクラス化された言語(クラスを持つほとんどすべての言語です)では十分ではありません。したがって、呼び出される関数を正確に知るには、Javaのfinal
アノテーションなどの追加情報が必要です。 。
HaskellはOO言語ではありませんが、f
をインライン化することでbar
の値を推測できます(これはstaticicallyです) main
をfoo
に置き換えてy
に置き換えます。foo
のmain
のターゲットは静的に認識されているため、呼び出しは静的にディスパッチされ、おそらく完全にインライン化されて最適化されます(これらの関数は小さいため、コンパイラーが小さいためそれら;一般的にそれを当てにすることはできませんが)。
したがって、コストは次のようになります。
非常に動的な言語を使用していて、多くの動的ディスパッチとコンパイラーが利用できる保証がほとんどない場合は、呼び出しごとにコストが発生します。 「非常に静的な」言語を使用している場合、成熟したコンパイラは非常に高速なコードを生成します。その中間にいる場合は、コーディングスタイルと実装のスマートさに依存します。
言語によっては、メソッド呼び出しのコストが高額になる可能性があることに注意してください。ほとんどの場合、読み取り可能なコードの記述とパフォーマンスの高いコードの記述の間にはトレードオフがあります。
残念ながら、これは次の要素に大きく依存しています。
まず、パフォーマンス最適化の第一法則はprofile firstです。ソフトウェア部分のパフォーマンスがスタック全体のパフォーマンスに無関係である多くのドメインがあります:データベース呼び出し、ネットワーク操作、OS操作、...
これは、ソフトウェアのパフォーマンスがまったく無関係であることを意味します。遅延が改善されなくても、ソフトウェアを最適化すると、エネルギーの節約とハードウェアの節約(またはモバイルアプリのバッテリーの節約)につながる可能性があります。
ただし、これらは通常、目立たないものにすることはできず、多くの場合、アルゴリズムの改善はマイクロ最適化よりも大幅に優先されます。
そのため、最適化する前に、最適化の対象を理解する必要があります...それが価値があるかどうか。
純粋なソフトウェアのパフォーマンスに関しては、ツールチェーンによって大きく異なります。
関数呼び出しには2つのコストがあります。
実行時のコストはかなり明白です。関数呼び出しを実行するには、ある程度の作業が必要です。たとえばx86でCを使用する場合、関数呼び出しには、(1)スタックへのレジスターのスピル、(2)レジスターへの引数のプッシュ、呼び出しの実行、および(3)スタックからのレジスターの復元が必要です。 関連する作業を確認するには、この呼び出し規約の概要 を参照してください。
このレジスタのスピル/復元には、かなりの時間がかかります(数十CPUサイクル)。
このコストは、関数を実行する実際のコストと比較するとわずかなものになると一般的に予想されていますが、ここでは逆効果になるパターンがあります。ゲッター、単純な条件で保護された関数などです。
インタープリターとは別に、プログラマーはコンパイラまたは[〜#〜] jit [〜#〜]が関数を最適化することを期待します不要な呼び出し。ただし、この希望は実を結ばない場合もあります。オプティマイザは魔法ではないからです。
オプティマイザは、関数呼び出しが簡単であることを検出し、inline呼び出し:基本的に、呼び出しサイトで関数の本体をコピー/貼り付けます。これは常に良い最適化とは限りません(膨らみを誘発する可能性があります)exposes contextをインライン化し、コンテキストがより多くの最適化を可能にするため、一般に価値があります。
典型的な例は次のとおりです。
_void func(condition: boolean) {
if (condition) {
doLotsOfWork();
}
}
void call() { func(false); }
_
func
がインライン化されている場合、オプティマイザーは分岐が行われないことを認識し、call
をvoid call() {}
に最適化します。
その意味で、(まだインライン化されていない場合)オプティマイザーから情報を非表示にすることによる関数呼び出しは、特定の最適化を阻害する可能性があります。非仮想化(実行時に最終的にどの関数が呼び出されるかを証明すること)は必ずしも容易ではないため、仮想関数呼び出しは特に有罪です。
結論として、私のアドバイスはclearlyを最初に記述して、アルゴリズムの早すぎる悲観化(キュービックな複雑さまたはより悪い咬傷)を避け、最適化が必要なものだけを最適化することです。
「メソッド呼び出しのコストは、言語によってはかなり大きくなる可能性があることに注意してください。ほとんどの場合、読み取り可能なコードの記述とパフォーマンスの高いコードの記述の間にはトレードオフがあります。」
パフォーマンスの高い最新のコンパイラーの豊富な業界を考えると、この引用されたステートメントは現在どのような条件下でまだ有効ですか?
絶対に言うつもりはないよ。そこに捨てるのは無謀だと思います。
もちろん、私は完全な真実を話しているわけではありませんが、それほど真実であることを気にしません。それはそのマトリックス映画のようです、それが1または2または3であったかどうか私は忘れました-それは大きなメロン(私は本当に最初は好きではなかった)が付いているセクシーなイタリアの女優と一緒だったと思いますオラクルの女性はキアヌ・リーブスにこう語りました「私はあなたが聞く必要があることをあなたに話しました」またはこの効果に対する何か、それが今私がしたいことです。
プログラマはこれを聞く必要はありません。プロファイラーの経験があり、見積もりがコンパイラにある程度当てはまる場合、彼らはすでにこれを知っており、プロファイリングの出力と特定のリーフコールがホットスポットである理由を理解することで、測定を通じて適切な方法で学習します。経験がなく、コードのプロファイルを作成したことがない場合、これは最後に聞く必要があることです。ホットスポットを特定する前に、すべてをインライン化するまで、コードの記述方法を非常に妥協する必要があります。より高性能になります。
とにかく、より正確な応答を得るには、状況によって異なります。条件のボートロードのいくつかは、すばらしい答えの中にすでにリストされています。 1つの言語を選択するだけで考えられる条件は、仮想呼び出しで動的ディスパッチに入る必要があるC++のように、それ自体がすでに巨大であり、いつ最適化してコンパイラーやリンカーでさえも最適化できる場合であり、試行することはもちろん、詳細な応答を保証するあらゆる可能な言語とコンパイラの条件に取り組むために。しかし、上に追加します"who cares?"パフォーマンスが重要な領域でレイトレーシングとして作業する場合でも、最初に最初に行う最後のことは、メソッドを手動でインライン化することです。任意の測定。
一部の人々は、測定の前にミクロの最適化を決して行うべきではないという提案に熱心すぎると思います。参照カウントの局所性をミクロ最適化として最適化する場合、パフォーマンス最適化が確実であることがわかっている領域(たとえば、レイトレーシングコード)でデータ指向の設計マインドセットを使用して、最初からそのような最適化を適用し始めます。そうでなければ、私は何年もこれらのドメインで働いた後すぐに大きなセクションを書き直さなければならないことを知っています。キャッシュヒットのデータ表現を最適化すると、線形への2次時間のように話さない限り、アルゴリズムの改善と同じ種類のパフォーマンスの改善が得られることがよくあります。
しかし、プロファイラーはインライン化のメリットを明らかにするのは得意ですが、インライン化しないことのメリットを明らかにするのではなく、測定の前にインライン化を開始する十分な理由を見たことはありません(インライン化しないと、実際にコードを高速化できます)インライン化されていない関数呼び出しはまれなケースであり、ホットコードのicacheの参照の局所性が向上し、場合によってはオプティマイザが実行の一般的なケースのパスに対してより良い仕事をすることもできます)。