多くの関数(巨大なリスト)を定義してコンパイルしました。また、関数ポインターを使用して、実行時に引数を動的に送信することで関数を呼び出して実行します。これは、反復ごとに 10 万を超える関数呼び出しを伴う反復プロセスです。コンパイルされた関数を呼び出す効率的な方法を知りたいです。私は自分の道が遅いと感じています。
7 に答える
これが問題かどうかを知るには、プログラムをプロファイリングする必要があります。個々の機能に 99% の時間を費やしている場合、期待できる最高の改善は 1% であり、それでさえありそうにありません。
関数呼び出しを高速化できる唯一の方法は、コンパイラが呼び出す関数を知っている場合です。
つまり、次のようなものです。
void foo(void)
{
/* do some stuff */
}
int main(void)
{
foo();
}
次のようにインライン化できます。
int main(void)
{
/* do some stuff */
}
しかし、コンパイラがどちらを呼び出すべきかわからない場合:
void foo(void)
{
/* do some stuff */
}
void bar(void)
{
/* do some other stuff */
}
typedef void(*Function)(void);
int main(void)
{
Function func = /* choose a function at runtime */
func();
}
コンパイラは、どの関数が呼び出されるかを予測できないため、インライン化できません。
コンパイラがサポートしている場合は、 を使用してみて__fastcall
ください。
この 1 つのレベルの間接化は、大きな違いを生むことはありません。コードをプロファイリングして、実際に速度が低下している場所を見つけます。
これは、これらの何十万もの関数のどれを呼び出すかを決定する方法によって異なります。関数ポインター リストを線形検索している場合は、おそらく多くの時間を無駄にしています。この場合、関数ポインタをハッシュ テーブルに入れるか、少なくとも並べ替えられたリストに格納してバイナリ検索を実行できるようにする必要があります。何をどのように行っているかについての情報がなければ、有益なアドバイスを提供することは困難です。
また、他の人が指摘しているように、必ずプロファイルを作成する必要があります。自分がやっていることが遅いかどうかわからないように思えます。その場合、最適化を試みる価値があるかどうかもわかりません。
関数呼び出しのオーバーヘッドは、主に次の組み合わせです。
- 関数呼び出し自体
- 渡すパラメータ
- 戻り値
- 関数を呼び出す必要がある回数
まず、次の質問をします。
- より少ない関数呼び出しを必要とするようにアルゴリズムを変更できますか?
- やり取りしなければならないデータの量を減らすことはできますか?
- 各関数への呼び出しをバッチ処理するようにアルゴリズムを変更できますか (1 回の呼び出しで値のグループを処理するか、少なくとも値のグループに対して同じ関数を繰り返し呼び出して、すべてのコードが CPU のキャッシュ メモリにとどまるようにします) )?
優れたアルゴリズムと効率的な実装ができたら、下位レベルの最適化方法に移行する必要があります。アセンブラーを使用して、スタックにプッシュするデータが少なくて済む独自の関数呼び出しプロトコルを実行できます。それらが「リーフ関数」(他の関数を呼び出さない) である場合、スタックを使用する必要さえない可能性があるため、呼び出しごとにオーバーヘッドのいくつかの命令を回避できます。(これのいくつかは、関数呼び出しを goto に置き換えることで、C で実行できる可能性がありますが、非常に醜いです)
最後に、自己変更コードの領域に入ります。関数を表すスニペットから新しいマシン コードを作成し、生成されたコードを呼び出します。ただし、これは非常にプロセッサ固有で扱いにくいものになる可能性があります-かなり低レベルです。
関数ポインタを逆参照するために必要な追加の命令の数は、関数の本体を構成する命令の数のごく一部である必要があります。すべての関数呼び出しを積極的にインライン化しても、大きな違いはありません。以前の回答で示唆されているように、ボトルネックを特定するにはプロファイラーを使用する必要があります。
物事の大きな計画では、ここまたはそこにいくつかの指示を削り取っても、大きな改善はありません。大きな勝利はあなたのアルゴリズムを改善することから来るでしょう。
独自の関数リンカーを作成して、特定の関数の「フラグメント」呼び出し順序をリンクし、それらをキャッシュしてオーバーヘッドを回避できます。あまり役に立たないかもしれませんが。
多くは関数のサイズに依存します。記憶やその他のあらゆる点で、それらが互いにどれだけ近いか。たとえば、2 番目の関数呼び出しがメモリ内の最初の関数呼び出しの直後にあった場合、その関数の開始は既にキャッシュされている可能性が高いため、関数ポインターを削除してもほとんど意味がありません。
あなたが私たちにいくつかの詳細を教えてくれたとしても、答えるのは簡単な質問ではありません.
マークが言うように... プロファイラーはあなたの友達です。