10

-O3 を指定して centos 上の gcc 4.6.2 でコンパイルされた次のプログラム:

#include <iostream>
#include <vector>
#include <algorithm>
#include <ctime>
using namespace std;

template <typename T>
class F {
public:
     typedef void (T::*Func)();

     F(Func f) : f_(f) {}

     void operator()(T& t) {
         (t.*f_)();
     }
private:
     Func f_;
};

struct X {
    X() : x_(0) {}

    void f(){
        ++x_;
    }

    int x_;
};

int main()
{
     const int N = 100000000;
     vector<X> xv(N);
     auto begin = clock();
     for_each (xv.begin(), xv.end(), F<X>(&X::f));
     auto end = clock();
     cout << end - begin << endl;
}

objdump -Dループ用に生成されたコードは次のとおりです。

  40097c:       e8 57 fe ff ff          callq  4007d8 <clock@plt>
  400981:       49 89 c5                mov    %rax,%r13
  400984:       0f 1f 40 00             nopl   0x0(%rax)
  400988:       48 89 ef                mov    %rbp,%rdi
  40098b:       48 83 c5 04             add    $0x4,%rbp
  40098f:       e8 8c ff ff ff          callq  400920 <_ZN1X1fEv>
  400994:       4c 39 e5                cmp    %r12,%rbp
  400997:       75 ef                   jne    400988 <main+0x48>
  400999:       e8 3a fe ff ff          callq  4007d8 <clock@plt>

明らかに、gcc は関数をインライン化しません。gcc がこの最適化に対応していないのはなぜですか? gcc に目的の最適化を実行させるコンパイラ フラグはありますか?

4

3 に答える 3

8

GCCは関数全体を最適化しようとしmainますが、失敗します(メモリの割り当て/解放xv、タイマー値の取得、入力/出力などのためのグローバル関数の多くの間接呼び出し)。したがって、次のように、コードを2つ(またはそれ以上)の独立した部分に分割してみることができます。

inline
void foobar(vector<X>& xv)
{
  for_each (xv.begin(), xv.end(), F<X>(&X::f));
}

int main()
{
  const int N = 100000000;
  vector<X> xv(N);
  auto begin = clock();
  foobar(xv);
  auto end = clock();
  cout << end - begin << endl;
}

これで、以前と同じ「同等の」コードができましたが、GCCのオプティマイザーの方が簡単に実行できるようになりました。ZN1X1fEv現在、アセンブラリストにの呼び出しはありません。

于 2012-04-16T15:41:12.440 に答える
7

これに関するいくつかの良い読み物は、スコット・アダムス・マイヤーズの効果的なC ++(第3版)項目30:インライン化の詳細を理解し、関数ポインターの呼び出しは決してインライン化されないと主張します。第3版は2008年に発行されましたが、2011年(おそらく2010年?)にリリースされたgcc 4.6以降、コンパイル時定数ポインターによってgccをインライン関数呼び出しにすることができました。ただし、これはCであり、注意が必要です。あるシナリオでは、__attribute__((flatten))呼び出しをインライン化する前に呼び出し元の関数を宣言する必要がありました(この状況では、関数ポインターを構造体のメンバーとして渡しました。このポインターは、関数呼び出しを行うインライン関数に渡されました。インライン化されたポインタ)。

つまり、いいえ、これはバグgccではありませんが、gcc(および/または他のコンパイラ)がいつかこれをインライン化できなくなる可能性があるという意味ではありません。しかし、本当の問題は、ここで実際に何が起こっているのか理解していないということだと思います。その理解を得るには、アセンブリプログラマーまたはコンパイラープログラマーのように考える必要があります。

タイプのオブジェクトを渡し、F<X>別のクラスのメンバー関数へのポインターを使用してオブジェクトを初期化します。インスタンスF<X>オブジェクトを定数として宣言しておらず、そのFunc f_メンバーを定数として宣言しておらず、void F::operator()(T& t)メンバーを定数として宣言していません。C ++言語レベルでは、コンパイラーはそれを非定数として処理する必要があります。それでも、後で最適化の段階で関数ポインターが変更されていないと判断できないという意味ではありませんが、この時点で非常に困難になっています。しかし、少なくともそれは地元の人です。F<X>オブジェクトがグローバルで宣言されていない場合、オブジェクトがstatic一定であると見なされることを完全に禁止します。

うまくいけば、これは、間接参照の実際の解決策としてではなく、関数ポインターによるインライン化の演習で行っているはずです。C ++で実際のパフォーマンスを実現したい場合は、型の力を使用します。具体的には、テンプレートパラメータをメンバー関数ポインタとして宣言すると、それは単なる定数ではなく、型の一部になります。この手法で関数呼び出しが生成されるケースは見たことがありません。

#include <iostream>
#include <vector>
#include <algorithm>
#include <ctime>
using namespace std;

template <typename T, void (T::*f_)()>
class F {
public:
     void operator()(T& t) {
         (t.*f_)();
     }
};

struct X {
    X() : x_(0) {}

    void f(){
        ++x_;
    }

    int x_;
};

int __attribute__((flatten)) main()
{
     const int N = 100000000;
     vector<X> xv(N);

     auto begin = clock();
     for_each (xv.begin(), xv.end(), F<X, &X::f>());
     auto end = clock();
     cout << end - begin << endl;

}
于 2012-06-17T22:49:23.650 に答える