Имеет ли одна виртуальная функция замедление работы всего класса?
Или только вызов виртуальной функции? И влияет ли скорость на скорость, если виртуальная функция действительно перезаписывается или нет, или это не имеет никакого эффекта, пока она виртуальная.
Наличие виртуальных функций замедляет работу всего класса, поскольку необходимо инициализировать, скопировать еще один элемент данных ... при работе с объектом такого класса. Для класса с полдюжиной членов или около того разница должна быть незначительной. Для класса, который содержит только один 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
в правильно набранном выражении.
Inside the C++ Object Model
автораStanley B. Lippman
. (Раздел 4.2, страницы 124-131)