Wenn Wenn Sie die Geschwindigkeit benötigen, sollten Sie in Erwägung ziehen, eine "Typ(-identifizierende) Nummer" in die Objekte einzubetten und eine Switch-Anweisung zu verwenden, um den typspezifischen Code auszuwählen. Auf diese Weise kann der Overhead für Funktionsaufrufe vollständig vermieden werden - es genügt ein lokaler Sprung. Schneller geht es nicht. Der Preis (in Bezug auf Wartbarkeit, Neukompilierungsabhängigkeiten usw.) besteht darin, dass die typspezifische Funktionalität lokalisiert werden muss (in der switch-Anweisung).
UMSETZUNG
#include <iostream>
#include <vector>
// virtual dispatch model...
struct Base
{
virtual int f() const { return 1; }
};
struct Derived : Base
{
virtual int f() const { return 2; }
};
// alternative: member variable encodes runtime type...
struct Type
{
Type(int type) : type_(type) { }
int type_;
};
struct A : Type
{
A() : Type(1) { }
int f() const { return 1; }
};
struct B : Type
{
B() : Type(2) { }
int f() const { return 2; }
};
struct Timer
{
Timer() { clock_gettime(CLOCK_MONOTONIC, &from); }
struct timespec from;
double elapsed() const
{
struct timespec to;
clock_gettime(CLOCK_MONOTONIC, &to);
return to.tv_sec - from.tv_sec + 1E-9 * (to.tv_nsec - from.tv_nsec);
}
};
int main(int argc)
{
for (int j = 0; j < 3; ++j)
{
typedef std::vector<Base*> V;
V v;
for (int i = 0; i < 1000; ++i)
v.push_back(i % 2 ? new Base : (Base*)new Derived);
int total = 0;
Timer tv;
for (int i = 0; i < 100000; ++i)
for (V::const_iterator i = v.begin(); i != v.end(); ++i)
total += (*i)->f();
double tve = tv.elapsed();
std::cout << "virtual dispatch: " << total << ' ' << tve << '\n';
// ----------------------------
typedef std::vector<Type*> W;
W w;
for (int i = 0; i < 1000; ++i)
w.push_back(i % 2 ? (Type*)new A : (Type*)new B);
total = 0;
Timer tw;
for (int i = 0; i < 100000; ++i)
for (W::const_iterator i = w.begin(); i != w.end(); ++i)
{
if ((*i)->type_ == 1)
total += ((A*)(*i))->f();
else
total += ((B*)(*i))->f();
}
double twe = tw.elapsed();
std::cout << "switched: " << total << ' ' << twe << '\n';
// ----------------------------
total = 0;
Timer tw2;
for (int i = 0; i < 100000; ++i)
for (W::const_iterator i = w.begin(); i != w.end(); ++i)
total += (*i)->type_;
double tw2e = tw2.elapsed();
std::cout << "overheads: " << total << ' ' << tw2e << '\n';
}
}
LEISTUNGSERGEBNISSE
Auf meinem Linux-System:
~/dev g++ -O2 -o vdt vdt.cc -lrt
~/dev ./vdt
virtual dispatch: 150000000 1.28025
switched: 150000000 0.344314
overhead: 150000000 0.229018
virtual dispatch: 150000000 1.285
switched: 150000000 0.345367
overhead: 150000000 0.231051
virtual dispatch: 150000000 1.28969
switched: 150000000 0.345876
overhead: 150000000 0.230726
Dies deutet darauf hin, dass ein Inline-Typ-Nummern-geschalteter Ansatz etwa (1,28 - 0,23) / (0,344 - 0,23) = 9.2 mal so schnell. Natürlich ist das spezifisch für das genaue System getestet / Compiler-Flags und Version usw., aber im Allgemeinen indikativ.
KOMMENTARE ZUM VIRTUELLEN VERSAND
Es muss jedoch gesagt werden, dass der Aufwand für virtuelle Funktionsaufrufe nur selten signifikant ist, und dann auch nur für oft so genannte triviale Funktionen (wie Getter und Setter). Selbst dann könnte man eine einzige Funktion zur Verfügung stellen, um eine ganze Menge Dinge auf einmal zu holen und zu setzen und so die Kosten zu minimieren. Die Leute machen sich viel zu viele Gedanken über die virtuelle Abfertigung - machen Sie also ein Profiling, bevor Sie unangenehme Alternativen finden. Das Hauptproblem bei ihnen ist, dass sie einen Funktionsaufruf außerhalb der Zeile durchführen, obwohl sie auch den ausgeführten Code delokalisieren, was die Cache-Nutzungsmuster verändert (zum Guten oder (häufiger) zum Schlechten).