¿Tener una sola función virtual ralentiza a toda la clase?
¿O solo la llamada a la función que es virtual? ¿Y la velocidad se ve afectada si la función virtual se sobrescribe o no, o esto no tiene ningún efecto mientras sea virtual?
Tener funciones virtuales ralentiza toda la clase en la medida en que un dato más tiene que ser inicializado, copiado,… cuando se trata de un objeto de tal clase. Para una clase con media docena de miembros aproximadamente, la diferencia debería ser insignificante. Para una clase que solo contiene un char
miembro, o ningún miembro, la diferencia puede ser notable.
Aparte de eso, es importante tener en cuenta que no todas las llamadas a una función virtual son llamadas a funciones virtuales. Si tiene un objeto de un tipo conocido, el compilador puede emitir código para una invocación de función normal, e incluso puede insertar dicha función en línea si lo desea. Solo cuando realiza llamadas polimórficas, a través de un puntero o referencia que podría apuntar a un objeto de la clase base o un objeto de alguna clase derivada, necesita la indirección vtable y paga por ella en términos de rendimiento.
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
}
Los pasos que debe seguir el hardware son esencialmente los mismos, sin importar si la función se sobrescribe o no. La dirección de la vtable se lee desde el objeto, el puntero de función se recupera de la ranura correspondiente y la función se llama mediante puntero. En términos de rendimiento real, las predicciones de rama pueden tener algún impacto. Entonces, por ejemplo, si la mayoría de sus objetos se refieren a la misma implementación de una función virtual dada, entonces existe la posibilidad de que el predictor de rama prediga correctamente a qué función llamar incluso antes de que se haya recuperado el puntero. Pero no importa qué función sea la común: podría ser la mayoría de los objetos delegando al caso base no sobrescrito, o la mayoría de los objetos que pertenecen a la misma subclase y, por lo tanto, delegando al mismo caso sobrescrito.
¿Cómo se implementan a un nivel profundo?
Me gusta la idea de jheriko para demostrar esto usando una implementación simulada. Pero usaría C para implementar algo similar al código anterior, de modo que el nivel bajo se vea más fácilmente.
clase padre 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!
}
barra de clase derivada
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
}
función f realizando una llamada de función virtual
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);
}
Como puede ver, una vtable es solo un bloque estático en la memoria, que en su mayoría contiene punteros de función. Cada objeto de una clase polimórfica apuntará a la vtable correspondiente a su tipo dinámico. Esto también hace que la conexión entre RTTI y las funciones virtuales sea más clara: puede verificar qué tipo es una clase simplemente mirando a qué vtable apunta. Lo anterior se simplifica de muchas maneras, como por ejemplo, herencia múltiple, pero el concepto general es sólido.
Si arg
es de tipo Foo*
y lo tomas arg->vtable
, pero en realidad es un objeto de tipo Bar
, obtendrás la dirección correcta del vtable
. Eso es porque vtable
siempre es el primer elemento en la dirección del objeto, sin importar si se llama vtable
o base.vtable
en una expresión escrita correctamente.
Inside the C++ Object Model
porStanley B. Lippman
. (Sección 4.2, página 124-131)