2013-05-22 9 views
5

私のコンピュータ(Intel i3-3220 @ 3.3GHz、Fedora 18)でベンチマークを実行したところ、予期しない結果が得られました。関数ポインタは、実際にはインライン関数より少し速いです。関数ポインタは、インライン関数より速く実行されます。どうして?

コード: 'toBigEndianPtr' は0.21から0.22秒かかりながら

g++ test.cpp -std=c++0x -O0 

でコンパイルさ

#include <iostream> 
#include <chrono> 
inline short toBigEndian(short i) 
{ 
    return (i<<8)|(i>>8); 
} 
short (*toBigEndianPtr)(short i)=toBigEndian; 
int main() 
{ 
    std::chrono::duration<double> t; 
    int total=0; 
    for(int i=0;i<10000000;i++) 
    { 
     auto begin=std::chrono::high_resolution_clock::now(); 
     short a=toBigEndian((short)i);//toBigEndianPtr((short)i); 
     total+=a; 
     auto end=std::chrono::high_resolution_clock::now(); 
     t+=std::chrono::duration_cast<std::chrono::duration<double>>(end-begin); 
    } 
    std::cout<<t.count()<<", "<<total<<std::endl; 
    return 0; 
} 

'toBigEndian' ループは、周りの0.26から0.27秒で常に終わります。

これをさらに奇妙にするのは、「合計」を削除すると、関数ポインタが0.35-0.37秒で遅くなり、インライン関数は約0.27-0.28秒になります。

私の質問は:

なぜ「合計」は存在するインライン関数よりも関数ポインタ速いのですか?

+5

最適化していません。最適化されていないコードのプロファイリングは無意味です。 –

+0

-O3では速度は変わりません。 – Hassedev

+1

私はそれを疑う... –

答えて

2

Oh s ** t(私はここで誓って検閲する必要がありますか?)、私はそれを見つけました。それはループの中にあるタイミングに何らかの形で関連していました。私が次のように外に移動したとき、結果はちょうどそうである通りです。

#include <iostream> 
#include <chrono> 
inline short toBigEndian(short i) 
{ 
    return (i<<8)|(i>>8); 
} 

short (*toBigEndianPtr)(short i)=toBigEndian; 
int main() 
{ 
    int total=0; 
    auto begin=std::chrono::high_resolution_clock::now(); 
    for(int i=0;i<100000000;i++) 
    { 
     short a=toBigEndianPtr((short)i); 
     total+=a; 
    } 
    auto end=std::chrono::high_resolution_clock::now(); 
    std::cout<<std::chrono::duration_cast<std::chrono::duration<double>>(end-begin).count()<<", "<<total<<std::endl; 
    return 0; 
} 

インラインで0.08秒、0。ポインターは20秒。あなたを気にして申し訳ありません。

7

短い回答:そうではありません。

  • 最適化していない(多く)-O0でコンパイルします。最適化がなければ、 "高速化"という言葉はありません。なぜなら、未定義コードはできるだけ速くないからです。
  • toBigEndianのアドレスを使用して、インライン展開を防止します。 inlineキーワードは、とにかく、コンパイラのヒントです。あなたはではなく、にそのベストを尽くしてそのヒントに従います。だから、

、何の意味あなたの測定値を与えるために、

  • は同じこと、インライン化されます1、
  • のADDRESを取られ、他のいずれかの操作を実行し、あなたのコード
  • 利用二つの機能を最適化します
+0

明らかに、私はコードを複数回実行したので、「周り」です。あなたの他の点は良いようだ。また、-O3はパフォーマンスにまったく影響しないようです。 – Hassedev

+0

私はあなたの答えを受け入れる必要があると思います。これはプログラム自体に関連することができないということはあまりにも不条理です。また、必要なプログラムではあまり頻繁に呼び出されないので、関数を最適化する必要はありません。 – Hassedev

+0

私はいくつかの間違った前提があったので、ループを何回も実行することについてのポイントを削除しました。 –

0

まず、-O0を指定すると、オプティマイザが実行されていないことになります。これは、自由であるため、コンパイラはインライン化のリクエストを無視していることを意味します。 2つの異なる呼び出しのコストはほぼ同じである必要があります。 -O2で試してください。

第2に、0.22秒しか実行していない場合、プログラムの起動に伴う変わった可変コストがテスト機能の実行コストを完全に支配します。その関数呼び出しはほんの数命令です。 CPUが2 GHzで動作している場合、その関数呼び出しを20ナノ秒のような時間で実行する必要があります。測定しているものであれば、その関数を実行するコストではありません。

テスト関数をループで1,000,000回呼び出してみてください。テストを実行するのに10秒以上かかるまで、ループの数を10倍大きくします。次に、結果をループ回数で除算して、操作コストの近似値を求めます。

+0

OK、私はループを500,000,000回作りました。関数ポインタは11.3秒、その他の関数は13.2秒です。 – Hassedev

3

パフォーマンスを測定する際のよくある間違いは、間違ったツールを使用して測定することです。 10000000回または500000000回の反復全体のパフォーマンスを測定していれば、std :: chronoを使用しても問題ありません。代わりに、toBigEndianのコール/インラインを測定するように求めています。 6つの命令すべてである関数。だから、私はrdtsc(タイムスタンプカウンタ、すなわちクロックサイクルを読む)に切り替えました。

コンパイラがループのすべてを実際に最適化できるようにして、小さな繰り返しごとに時間を記録するのではなく、コードシーケンスが異なります。今、g++ -O3 fp_test.cpp -o fp_test -std=c++11でコンパイルした後、私は望む効果を観察します。インラインバージョンは、反復ごとに約2.15サイクルを平均し、関数ポインタは反復ごとに約7.0サイクルかかります。

rdtscを使用しなくても、違いはまだかなりわかります。壁時計の時間は、インラインコードの場合は360ms、関数ポインタの場合は1.17秒でした。ですから、このコードではrdtscの代わりにstd :: chronoを使うことができます。

変更されたコードは、次のとおりです。多くの/ほとんどの自尊心の最近のコンパイラには

#include <iostream> 
static inline uint64_t rdtsc(void) 
{ 
    uint32_t hi, lo; 
    asm volatile ("rdtsc" : "=a"(lo), "=d"(hi)); 
    return ((uint64_t)lo)|(((uint64_t)hi)<<32); 
} 
inline short toBigEndian(short i) 
{ 
    return (i<<8)|(i>>8); 
} 
short (*toBigEndianPtr)(short i)=toBigEndian; 
#define LOOP_COUNT 500000000 
int main() 
{ 
    uint64_t t = 0, begin=0, end=0; 
    int total=0; 
    begin=rdtsc(); 
    for(int i=0;i<LOOP_COUNT;i++) 
    { 
     short a=0; 
     a=toBigEndianPtr((short)i); 
     //a=toBigEndian((short)i); 
     total+=a; 
    } 
    end=rdtsc(); 
    t+=(end-begin); 
    std::cout<<((double)t/LOOP_COUNT)<<", "<<total<<std::endl; 
    return 0; 
} 
+0

秒の代わりにサイクルを測定するという素晴らしいアイデアをありがとう。 – Hassedev

-1

、あなたはまだそれがポインタを通じて呼び出されても、関数呼び出しをインライン化します投稿コード。 (コンパイラがコードを最適化するために妥当な努力をしていると仮定します)。状況は見通しが分かりにくいです。言い換えれば、生成されたコードはどちらの場合でも簡単にほぼ同じになります。これは、テストが測定しようとしているものを測定するのに本当に役立たないことを意味します。

本当に呼び出しが物理的にポインタによって実行されていることを確認したい場合は、コンパイル時にポインタ値を把握できないポイントにコンパイラを混乱させるように努力する必要があります。たとえば、

toBigEndianPtr = rand() % 1000 != 0 ? toBigEndian : NULL; 

などのように、これらの行に沿ってポインタ値を実行時に依存させます。また、関数ポインタをvolatileと宣言することもできます。通常は、毎回真のスルー・ポインタ呼び出しが行われるだけでなく、各繰り返しでメモリからポインタ値を再読み込みするようコンパイラに指示されます。

関連する問題