単一の仮想関数を使用すると、クラス全体が遅くなりますか?
それとも仮想関数の呼び出しだけですか?また、仮想関数が実際に上書きされているかどうかに関係なく、速度が影響を受けますか、それとも仮想である限り、効果はありません。
そのようなクラスのオブジェクトを処理するときに、データの1つ以上の項目を初期化、コピーする必要がある限り、仮想関数を使用すると、クラス全体の速度が低下します。半ダースほどのメンバーがいるクラスの場合、その差はごくわずかです。単一のchar
メンバーのみを含むクラス、またはメンバーをまったく含まないクラスでは、違いが顕著になる場合があります。
それとは別に、仮想関数へのすべての呼び出しが仮想関数呼び出しであるとは限らないことに注意することが重要です。既知のタイプのオブジェクトがある場合、コンパイラーは通常の関数呼び出しのコードを発行できます。また、必要に応じて、関数をインライン化することもできます。基本クラスのオブジェクトまたはいくつかの派生クラスのオブジェクトを指す可能性があるポインターまたは参照を介してポリモーフィックな呼び出しを行う場合にのみ、vtableの間接参照が必要であり、パフォーマンスの観点からそれを支払います。
struct Foo { virtual ~Foo(); virtual int a() { return 1; } };
struct Bar: public Foo { int a() { return 2; } };
void f(Foo& arg) {
Foo x; x.a(); // non-virtual: always calls Foo::a()
Bar y; y.a(); // non-virtual: always calls Bar::a()
arg.a(); // virtual: must dispatch via vtable
Foo z = arg; // copy constructor Foo::Foo(const Foo&) will convert to Foo
z.a(); // non-virtual Foo::a, since z is a Foo, even if arg was not
}
関数が上書きされているかどうかに関係なく、ハードウェアが実行する必要のある手順は基本的に同じです。vtableのアドレスは、オブジェクト、適切なスロットから取得された関数ポインター、およびポインターによって呼び出された関数から読み取られます。実際のパフォーマンスに関しては、分岐予測が何らかの影響を与える可能性があります。したがって、たとえば、ほとんどのオブジェクトが特定の仮想関数の同じ実装を参照している場合、ポインターが取得される前でも、分岐予測器がどの関数を呼び出すかを正しく予測する可能性があります。ただし、どの関数が一般的なものであるかは関係ありません。ほとんどのオブジェクトは、上書きされていない基本ケースに委任されているか、ほとんどのオブジェクトが同じサブクラスに属しているため、同じ上書きされたケースに委任されている可能性があります。
深いレベルでどのように実装されていますか?
私はジェリコのアイデアがモック実装を使用してこれを実証するのが好きです。しかし、私はCを使用して上記のコードに似たものを実装し、低レベルがより見やすくなるようにします。
親クラスFoo
typedef struct Foo_t Foo; // forward declaration
struct slotsFoo { // list all virtual functions of Foo
const void *parentVtable; // (single) inheritance
void (*destructor)(Foo*); // virtual destructor Foo::~Foo
int (*a)(Foo*); // virtual function Foo::a
};
struct Foo_t { // class Foo
const struct slotsFoo* vtable; // each instance points to vtable
};
void destructFoo(Foo* self) { } // Foo::~Foo
int aFoo(Foo* self) { return 1; } // Foo::a()
const struct slotsFoo vtableFoo = { // only one constant table
0, // no parent class
destructFoo,
aFoo
};
void constructFoo(Foo* self) { // Foo::Foo()
self->vtable = &vtableFoo; // object points to class vtable
}
void copyConstructFoo(Foo* self,
Foo* other) { // Foo::Foo(const Foo&)
self->vtable = &vtableFoo; // don't copy from other!
}
派生クラスBar
typedef struct Bar_t { // class Bar
Foo base; // inherit all members of Foo
} Bar;
void destructBar(Bar* self) { } // Bar::~Bar
int aBar(Bar* self) { return 2; } // Bar::a()
const struct slotsFoo vtableBar = { // one more constant table
&vtableFoo, // can dynamic_cast to Foo
(void(*)(Foo*)) destructBar, // must cast type to avoid errors
(int(*)(Foo*)) aBar
};
void constructBar(Bar* self) { // Bar::Bar()
self->base.vtable = &vtableBar; // point to Bar vtable
}
仮想関数呼び出しを実行する関数f
void f(Foo* arg) { // same functionality as above
Foo x; constructFoo(&x); aFoo(&x);
Bar y; constructBar(&y); aBar(&y);
arg->vtable->a(arg); // virtual function call
Foo z; copyConstructFoo(&z, arg);
aFoo(&z);
destructFoo(&z);
destructBar(&y);
destructFoo(&x);
}
ご覧のとおり、vtableはメモリ内の静的ブロックであり、ほとんどが関数ポインタを含んでいます。ポリモーフィッククラスのすべてのオブジェクトは、その動的型に対応するvtableを指します。これにより、RTTIと仮想関数の間の接続もより明確になります。クラスが指しているvtableを調べるだけで、クラスのタイプを確認できます。上記は、多重継承など、多くの点で単純化されていますが、一般的な概念は適切です。
場合arg
タイプでありFoo*
、あなたが取るarg->vtable
が、実際に型のオブジェクトであるBar
場合、あなたはまだの正しいアドレスを取得しますvtable
。これvtable
は、それが呼び出されたvtable
かbase.vtable
、正しく型指定された式であるかに関係なく、常にオブジェクトのアドレスの最初の要素だからです。
Inside the C++ Object Model
によって傑作を読むことをお勧めしStanley B. Lippman
ます。(セクション