2011-08-05 33 views
0

私が現在扱っているアプリケーションでは、何十億という小さな関数を何度も呼び出すいくつかの力任せの数値アルゴリズムを使用しています。傾斜と静的多型を使った関数呼び出しをなくすことで、パフォーマンスをどれだけ向上させることができるのか、私は迷っていました。関数呼び出しコスト

次のような状況で非インラインおよび非組み込み関数を呼び出す機能を相対的に呼び出しのコストは何ですか:

1)関数呼び出し関数ポインタを経由して

2)仮想関数呼び出し

私はそれを測定するのは難しいと知っていますが、非常に大まかな見積もりがあります。

ありがとうございました!仮想関数コンパイラを呼び出すために

Fetch address of function -> Call function 

はする必要があります::仮想メカニズムはコンパイラの実装の詳細であること、だから、実装:

Fetch address of vptr -> Fetch address of the function -> Call function 

注意をメンバ関数の呼び出しコンパイラがする必要があるようにするに

+8

現代のコンパイラは、最近、 'inline'を指定しなくても小さな関数をインラインでインライン展開します。アプリケーションをプロファイリングしたり、生成されたアセンブリ(最適化スイッチをオンにして構築)を見ましたか?そして、コード/コンパイラの設定を見ることなく、私たちが言うことは何でも投機的です。 –

+1

プロファイリングとボトルネックを探すのはどうですか? – littleadv

+0

@In silico。投機は、問題を理解している人から来ている限り、問題ありません。私はasmを知らない。 – pic11

答えて

3

コンパイラごとに異なる場合がありますが、vptrまたはvtableもありません。コンパイラはvptrvtableでそれを実装しています。

確かにいくつかのオーバーヘッドがあります(追加でFetchが1つ追加されます)。影響がどれくらいあるかを正確に知るには、ソースコードをプロファイルする必要はありません。

+0

vtableのアドレスは、ほとんどの場合、すでにレジスタにロードされています( 'this'として)。これにより、仮想通話のコストが削減されます。 –

+0

@Bo Persson:私は、コンパイラがこの場合にどのように最適化するかわからないことを認めています。その新しいディメンションを視点に追加してくれてありがとう。 –

+0

これは少し誤解を招くことです。最新のCPUはパイプライン化されています。 1回目と2回目のフェッチの違いは、2回目のフェッチが最初のフェッチに依存するため、2倍ではありません。つまり、早くフェッチを開始することはできません。 – MSalters

0

AMDのコードアナライザー(IBSとTBSを使用)のようなプロファイラーを使用するだけで、もっとハードコアのルートになり、Agner Fogの最適化マニュアルを読むことができます(正確な命令タイミングとコードの最適化に役立ちます) :http://www.agner.org/optimize/

2

対象アーキテクチャとコンパイラによって異なりますが、小さなテストを作成して生成されたアセンブリをチェックすることができます。あなただけのmain.cppにするためにアセンブリをチェックする必要があるので、あなたも、TEST.CPPを記述する必要はありません

// test.h 
#ifndef FOO_H 
#define FOO_H 

void bar(); 

class A { 
public: 
    virtual ~A(); 
    virtual void foo(); 
}; 

#endif 

// main.cpp 
#include "test.h" 

void doFunctionPointerCall(void (*func)()) { 
    func(); 
} 

void doVirtualCall(A *a) { 
    a->foo(); 
} 

int main() { 
    doFunctionPointerCall(bar); 

    A a; 
    doVirtualCall(&a); 

    return 0; 
} 

注:

は、私がテストを行うには1をしました。

gcc main.cpp -S -O3 

それはアセンブリ出力で、ファイルmain.sを作成します:gccがフラグ-Sを使用すると、コンパイラのアセンブリ出力を表示するには

。 これで、gccが呼び出しに生成した内容を確認できます。

doFunctionPointerCall:

.globl _Z21doFunctionPointerCallPFvvE 
    .type _Z21doFunctionPointerCallPFvvE, @function 
_Z21doFunctionPointerCallPFvvE: 
.LFB0: 
    .cfi_startproc 
    jmp *%rdi 
    .cfi_endproc 
.LFE0: 
    .size _Z21doFunctionPointerCallPFvvE, .-_Z21doFunctionPointerCallPFvvE 

doVirtualCall:

.globl _Z13doVirtualCallP1A 
    .type _Z13doVirtualCallP1A, @function 
_Z13doVirtualCallP1A: 
.LFB1: 
    .cfi_startproc 
    movq (%rdi), %rax 
    movq 16(%rax), %rax 
    jmp *%rax 
    .cfi_endproc 
.LFE1: 
    .size _Z13doVirtualCallP1A, .-_Z13doVirtualCallP1A 

注ここで私はアセンブリは、他のachitecturesのために変更されること、x86_64版を使用しています。

アセンブリを見れば、仮想呼び出しに2つの余分なmovqが使用されているように見えますが、おそらくvtableにオフセットがあります。実際のコードでは、いくつかのレジスタ(関数ポインタか仮想呼び出しか)を保存する必要がありますが、仮想呼び出しにはさらに2つのmovq over関数ポインタが必要です。

+0

違いは、関数ポインタでどの関数を呼び出すかを選択していることです。そのためのコードを追加する必要があります。 –

0

関数呼び出しが小さい場合、関数呼び出しは大きなオーバーヘッドになります。最近のCPUで最適化されたCALLとRETURNは、多くの呼び出しが行われても顕著になります。また、小さな関数がメモリ全体に広がってCALL/RETURNがキャッシュミスや過度のページングを引き起こす可能性があります。

//code 
int Add(int a, int b) { return a + b; } 
int main() { 
    Add(1, Add(2, 3)); 
... 
} 

// NON-inline x86 ASM 
Add: 
    MOV eax, [esp+4] // 1st argument a 
    ADD eax, [esp+8] // 2nd argument b 
    RET 8 // return and fix stack 2 args * 4 bytes each 
    // eax is the returned value 

Main: 
    PUSH 3 
    PUSH 2 
    CALL [Add] 
    PUSH eax 
    PUSH 1 
    CALL [Add] 
... 

// INLINE x86 ASM 
Main: 
    MOV eax, 3 
    ADD eax, 2 
    ADD eax, 1 
... 

最適化を目標にしていて、多くの小さな関数を呼び出す場合は、常にインライン化するのがベストです。申し訳ありませんが、私はc/C++コンパイラで使用される醜いASM構文を気にしません。

関連する問題