4

enumメンバー変数を持つクラスがあります。メンバー関数の1つは、これに基づいて動作するenumため、「可能な」最適化として、2つの異なる動作を2つの異なる関数として持ち、クラスに構築時に設定されるメンバー関数ポインターを与えます。私はこの状況を次のようにシミュレートしました:

enum catMode {MODE_A, MODE_B};

struct cat
{
    cat(catMode mode) : stamp_(0), mode_(mode) {}

    void
    update()
    {
        stamp_ = (mode_ == MODE_A) ? funcA() : funcB();
    }

    uint64_t stamp_;
    catMode  mode_;
};

struct cat2
{
    cat2(catMode mode) : stamp_(0), mode_(mode)
    {
        if (mode_ = MODE_A)
            func_ = funcA;
        else
            func_ = funcB;
    }

    void
    update()
    {
        stamp_ = func_();
    }

    uint64_t stamp_;
    catMode  mode_;
    uint64_t (*func_)(void);
};

次に、catオブジェクトと長さの配列を作成します32。配列をトラバースしてキャッシュに入れ、次にcats updateメソッド時間を呼び出し、配列で32使用してレイテンシを保存しrdtscます...

rand()次に、、、を使用して数百回ループする関数を呼び出し、ulseep()任意strcmp()の..が戻ってきて、32もう一度やり直します。

その結果、ブランチを使用するメソッドは常に約44+/-10サイクルであるように見えますが、関数ポインターを使用するメソッドは約+/-サイクルになる傾向があります130。なぜそうなるのか知りたいのですが?

どちらかといえば、私は同様のパフォーマンスを期待していました。また、その1つの関数に対する実際の猫のクラスの完全な特殊化はやり過ぎになるため、テンプレート化はほとんどオプションではありません。

4

1 に答える 1

4

完全なSSCCEがなければ、私は通常そのような質問で行うのと同じ方法でこれに取り組むことはできません。
だから私ができる最善のことは推測することです:

2つのケースの主な違いは、ブランチと関数ポインタがあることです。funcA()違いがまったく見られないという事実は、それfuncB()が非常に小さな関数であることを強く示唆しています。

可能性#1:

コードのブランチバージョンでfuncA()funcB()コンパイラによってインライン化されています。これにより、関数呼び出しのオーバーヘッドがスキップされるだけでなく、関数が十分に些細なものである場合は、ブランチを完全に最適化することもできます。

一方、関数ポインタは、コンパイラがコンパイル時に解決できない限り、インライン化できません。

可能性#2:

分岐を関数ポインターと比較することにより、分岐予測子分岐ターゲット予測子と比較します。

分岐ターゲット予測は、分岐予測と同じではありません。分岐の場合、プロセッサは分岐する方法を予測する必要があります。関数ポインタの場合、分岐先を予測する必要があります。

プロセッサの分岐予測は、分岐ターゲット予測よりもはるかに正確である可能性が非常に高くなります。しかし、繰り返しになりますが、これはすべて当て推量です...

于 2012-07-25T15:31:28.000 に答える