5

私の理解では、C++ 仮想呼び出しの場合、次のことが必要です。

  1. シンボル テーブルからオブジェクトの型を取得します
  2. 型テーブルから v テーブルを取得する
  3. v-table の関数シグネチャを使用して関数を検索します
  4. 関数を呼び出します。

非仮想 (C など) の呼び出しの場合は、#4 のみが必要です。

#3が最も時間がかかると思います。C++ でのリアルタイム オーバーライドの性質を考えると、上記の手順でコンパイル時間を最適化できる可能性はあまりありませんでした。したがって、長い関数シグネチャを持つ複雑なクラス継承の場合、C++ 仮想呼び出しは非仮想呼び出しよりもはるかに遅くなるはずです。

しかし、すべての主張は反対です。なぜですか?

4

6 に答える 6

7
  1. シンボル テーブルからオブジェクトの型を取得します
  2. 型テーブルから v テーブルを取得する
  3. v-table の関数シグネチャを使用して関数を検索します
  4. 関数を呼び出します。

これは、v-table ベースのディスパッチがどのように機能するかをよく理解していません。それははるかに簡単です:

  1. オブジェクト ポインターから v-table を取得します。問題の関数に適した v テーブルを選択します (複数の基本クラスが使用されている場合)。
  2. コンパイル時に決定される特定のオフセットをこの v-table ポインターに追加して、特定の関数ポインターをフェッチします。
  3. その関数ポインタを呼び出します。

各オブジェクトには、そのオブジェクトの元の型の v-table を指す v-table ポインターがあります。したがって、「シンボル テーブル」から型を取得する必要はありません。v テーブルの検索は必要ありません。コンパイル時に提供される関数シグネチャに基づいて、v テーブル内のどのポインターにアクセスする必要があるかは、コンパイル時に正確に判断できます。コンパイラがクラス内の各仮想関数にインデックスを付ける方法がすべてです。各仮想関数の特定の順序を決定できるため、コンパイラがそれを呼び出すときに、どの関数を呼び出すかを決定できます。

なので全体的に速いです。

仮想基本クラスを扱う場合は少し複雑になりますが、一般的な考え方は同じです。

于 2012-12-05T04:41:30.533 に答える
4

1 & 2) 「シンボル テーブル」からオブジェクトの型を取得する必要はありません。v-table は通常、オブジェクトの非表示フィールドによってポイントされます。したがって、v-table の取得は基本的に 1 つのポインター間接化です。

3) v-table は「検索」されません。各仮想関数には、コンパイル時に決定される v-table 内の固定インデックス/オフセットがあります。したがって、これは基本的にポインターからのオフセットからのフェッチです。

したがって、直接の C スタイルの呼び出しよりも遅くなりますが、あなたが提案するほど難しくはありません。これは、C の次のようなものに似ています。

struct MyObject_vtable {
    int (*foo)();
    void (*bar)(const char *arg);
};

struct MyObject {
    int m_instanceVariable1;
    int m_instanceVariable2;
    struct MyObject_vtable *__vtable;
};

struct MyObject * obj = /* ... construct a MyObject instance */;

// int result = obj->foo();
int result = (*(obj->__vtable.foo))();

// obj->bar("Hello");
(*(obj->__vtable.bar))("Hello");

また、これは質問の範囲を少し超えているかもしれませんが、多くの場合、コンパイラーはコンパイル時に呼び出される関数を決定できることに注意してください。そのような場合、関数を直接呼び出すことができます。仮想呼び出し機構。例えば:

MyObject obj1;
int result1 = obj1.foo();

MyObject *obj2 = getAMyObject();
int result2 = obj2->foo();

foo()この場合、最初の呼び出しでどちらを呼び出すかはコンパイル時にわかっているため、直接呼び出すことができます。2 番目の呼び出しでは、が をオーバーライドgetAMyObject()した派生クラスのオブジェクトを返す可能性があるため、仮想呼び出しメカニズムを使用する必要があります。MyObjectfoo()

于 2012-12-05T04:39:51.847 に答える
4

通常の関数呼び出しに対する仮想関数呼び出しのオーバーヘッドは、2 つの余分なfetch操作です (1 つは v ポインターの値を取得するため、もう 1 つはメソッドのアドレスを取得するためです)。
ほとんどの場合、このオーバーヘッドは、パフォーマンス プロファイリングで表示できるほど重要ではありません。

また、場合によっては、virtual呼び出される関数がコンパイル時に決定できる場合、スマート コンパイラは実行時にではなく決定します。

于 2012-12-05T04:34:31.077 に答える
2

それは、実際には、ボトルネックの問題です...


...しかし、最初に、図 (64 ビット) を使用して、仮定を修正しましょう。オブジェクト モデルは実装固有ですが、Itanium ABI (gcc、clang、icc など) で使用される仮想テーブルの概念は、C++ で比較的普及しています。

class Base { public: virtual void foo(); int i; };

+-------+---+---+
| v-ptr | i |pad|
+-------+---+---+

class Derived: public Base { public: virtual void foo(); int j; };

+-------+---+---+
| v-ptr | i | j |
+-------+---+---+

単一の (非仮想) 基本クラスの場合、v-ptrはオブジェクトの最初のメンバーです。したがって、v-ptr を取得するのは簡単です。それ以降、オフセットは (コンパイル時に) わかっているため、これは単なるポインター演算であり、その後にポインター逆参照による関数呼び出しが続きます。

LLVM のおかげでライブで見てみましょう:

%class.Base = type { i32 (...)**, i32 }
                     ~~~~~~~~~~^  ^~~
                     v-ptr          i

%class.Derived = type { [12 x i8], i32 }
                        ~~~~~~~~^  ^~~
                        Base         j

define void @_Z3fooR4Base(%class.Base* %b) uwtable {
  %1 = bitcast %class.Base* %b to void (%class.Base*)***
  %2 = load void (%class.Base*)*** %1, align 8
  %3 = load void (%class.Base*)** %2, align 8
  tail call void %3(%class.Base* %b)
  ret void
}
  • %1: v-table へのポインター (ビットキャストによって取得され、CPU に関して透過的です)
  • %2: v-table 自体
  • %3: Derived::foo(テーブルの最初の要素)へのポインター
于 2012-12-05T08:19:22.337 に答える
1

これは基本的に 2 つの読み取り (1 つはオブジェクト インスタンスから vtable ptr を取得するため、もう 1 つは vtable から関数ポインターを取得するため) と関数呼び出しです。多くの場合、メモリはかなり熱く、キャッシュにとどまります。分岐がないため、CPU はこれを非常にうまくパイプライン処理して、多くの費用を隠すことができます。

于 2012-12-05T04:36:04.943 に答える
0

C での動的ポリモーフィズムの例は、手順を説明するのに役立つかもしれません。これらのクラスが C++ にあるとします。

struct Base {
  int someValue;
  virtual void bar();
  virtual int foo();
  void foobar();
};

struct Derived : Base {
  double someOtherValue;
  virtual void bar();
};

C では、同じ階層を次のように実装できます。

struct Base {
  void** vtable;
  int someValue;
};

void Base_foobar(Base* p);
void Base_bar_impl(Base* p);
int Base_foo_impl(Base* p);

void* Base_vtable[] = {(void*)&Base_bar_impl, (void*)&Base_foo_impl};

void Base_construct(Base* p) {
  p->vtable = Base_vtable;
  p->someValue = 0;
};

void Base_bar(Base* p) {
  (void(*)())(p->vtable[0])();  // this is the virtual dispatch code for "bar".
};

int Base_foo(Base* p) {
  return (int(*)())(p->vtable[1])();  // this is the virtual dispatch code for "foo".
};


struct Derived {
  Base base;
  double someOtherValue;
};

void Derived_bar_impl(Base* p);

void* Derived_vtable[] = {(void*)&Derived_bar_impl, (void*)&Base_foo_impl};

void Derived_construct(Derived* p) {
  Base_construct(&(p->base));
  p->base.vtable = Derived_vtable;  // setup the new vtable as part of derived-class constructor.
  p->someOtherValue = 0.0;
};

明らかに、シンタックスは C++ の方がはるかに単純ですが (当たり前!)、ご覧のとおり、動的ディスパッチに関して複雑なことは何もありません。オブジェクトの構築時に設定されます。また、コンパイラが自動的に実行することが難しいことは何もありません (つまり、コンパイラは上記の C++ コードを簡単に取得して、対応する C コードを以下に生成できます)。多重継承の場合も同様に簡単で、各基本クラスには独自の vtable ポインターがあり、派生クラスはその基本クラスごとにそれらのポインターを設定する必要があります。必要なスティッキー ポイントはこれだけです。階層を上下にキャストするときにポインター オフセットを適用します (したがって、C++ スタイルのキャスト演算子を使用することが重要です!)。

一般に、真面目な人が仮想関数のオーバーヘッドについて議論するとき、彼らは関数呼び出しを行うために必要な「複雑な」手順について話しているわけではありません (それはかなり些細なことであり、時には最適化されていないためです)。彼らはおそらく、(ディスパッチされた呼び出しを予測するのが難しいため) プリフェッチャーを破棄したり、コンパイラーが最終的な実行可能ファイルで必要な場所の近く (またはインライン) に関数をパッケージ化するのを妨げたりするなど、キャッシュ関連の問題について話している (またはDLL)。これらの問題は仮想関数の主なオーバーヘッドですが、それでもそれほど重要ではありません。一部のコンパイラは、これらの問題をかなりうまく軽減するのに十分スマートです。

于 2012-12-05T05:07:10.303 に答える