Замедляет ли наличие одной виртуальной функции весь класс?
Или только вызов виртуальной функции? И влияет ли скорость, если виртуальная функция действительно перезаписана или нет, или она не оказывает влияния, пока она виртуальная.
Наличие виртуальных функций замедляет весь класс, поскольку еще один элемент данных должен быть инициализирован, скопирован ... при работе с объектом такого класса. Для класса с полдюжиной членов или около того, разница должна быть незначительной. Для класса, который содержит только один 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 считывается из объекта, указатель функции извлекается из соответствующего слота, а функция вызывается указателем. С точки зрения фактической производительности прогнозы ветвления могут оказать некоторое влияние. Так, например, если большинство ваших объектов ссылаются на одну и ту же реализацию данной виртуальной функции, то есть некоторый шанс, что предиктор ветвления правильно предскажет, какую функцию вызвать, даже до того, как указатель будет получен. Но не имеет значения, какая функция является общей: это может быть большинство объектов, делегирующих не перезаписанному базовому случаю, или большинство объектов, принадлежащих одному и тому же подклассу и, следовательно, делегирующих одному и тому же перезаписанному регистру.
как они реализованы на глубоком уровне?
Мне нравится идея jheriko продемонстрировать это с помощью фиктивной реализации. Но я бы использовал 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
в правильно набранном выражении.