Auswirkungen auf die Leistung der tiefen Vererbungsstruktur in C ++

8

Gibt es irgendeinen Effizienznachteil, der mit tiefen Vererbungsbäumen (in c ++) verbunden ist, d. h. eine große Menge von Klassen A, B, C und so weiter, so dass B erweitert A, C erweitert B und so eins. Eine Effizienz-Implikation, die ich mir vorstellen kann, ist, dass wenn wir die unterste Klasse instanziieren, sagen wir C, dann werden auch die Konstruktoren von B und A aufgerufen, was Auswirkungen auf die Leistung haben wird.

    
SegFault 05.11.2011, 06:38
quelle

3 Antworten

12

Lassen Sie uns die Operationen aufzählen, die wir in Betracht ziehen sollten:

Konstruktion / Zerstörung

Jeder Konstruktor / Destruktor ruft seine Basisklassenäquivalente auf. Wie James McNellis jedoch schon sagte, haben Sie diese Arbeit offensichtlich trotzdem gemacht. Du bist nicht von A abgeleitet, nur weil es da war. So wird die Arbeit auf die eine oder andere Weise erledigt.

Ja, es werden einige weitere Funktionsaufrufe benötigt. Der Funktionsaufruf-Overhead ist jedoch nichts im Vergleich zu der tatsächlichen Arbeit, die eine wesentlich tiefere Klassenhierarchie tatsächlich leisten müsste. Wenn Sie an dem Punkt sind, an dem der Funktionsaufruf-Overhead tatsächlich für die Leistung wichtig ist, würde ich sehr empfehlen, dass das Aufrufen von Konstruktoren überhaupt nicht das ist, was Sie in diesem Code machen möchten.

Objektgröße

Im Allgemeinen ist der Aufwand für eine abgeleitete Klasse nichts. Der Aufwand für virtuelle Member ist ein Zeiger oder für die virtuelle Vererbung.

Mitgliederfunktionsaufrufe, Static

Damit meine ich den Aufruf nicht virtueller Member-Funktionen oder das Aufrufen virtueller Member-Funktionen mit Klassennamen (ClassName :: FunctionName-Syntax). Beide ermöglichen dem Compiler, zur Kompilierzeit zu wissen, welche Funktion aufgerufen werden soll.

Die Leistung ist invariant mit der Größe der Hierarchie, da die Kompilierzeit festgelegt ist.

Mitgliederfunktionsaufrufe, dynamisch

Dies ruft virtuelle Funktionen mit der vollen und vollständigen Erwartung von Laufzeitaufrufen auf.

Bei den meisten vernünftigen C ++ - Implementierungen ist dies invariant mit der Größe der Objekthierarchie. Die meisten Implementierungen verwenden für jede Klasse eine v-Tabelle. Jedes Objekt hat einen v-Tabellenzeiger als Mitglied. Für jeden bestimmten dynamischen Aufruf greift der Compiler auf den Zeiger der v-Tabelle zu, wählt die Methode aus und ruft sie auf. Da die v-Tabelle für jede Klasse gleich ist, ist sie für eine Klasse, die eine tiefe Hierarchie hat, nicht langsamer als eine mit einer flachen Hierarchie.

Virtuelle Vererbung spielt ein bisschen damit.

Zeiger-Cast, statisch

Dies bezieht sich auf static_cast oder eine gleichwertige Operation. Dies bedeutet die implizite Umwandlung von einer abgeleiteten Klasse in eine Basisklasse, die explizite Verwendung von static_cast oder C-artigen Umwandlungen, etc.

Beachten Sie, dass dies technisch das Referenzcasting einschließt.

Die Leistung von statischen Umwandlungen zwischen Klassen (nach oben oder nach unten) ist invariant mit der Größe der Hierarchie. Alle Zeigeroffsets werden zur Kompilierzeit generiert. Dies sollte sowohl für die virtuelle als auch für die nicht virtuelle Vererbung gelten, aber ich bin mir nicht 100% sicher.

Zeiger, dynamisch

Dies bezieht sich offensichtlich auf die explizite Verwendung von dynamic_cast . Dies wird normalerweise verwendet, wenn von einer Basisklasse in eine abgeleitete Klasse umgewandelt wird.

Die Leistung von dynamic_cast wird sich wahrscheinlich für eine große Hierarchie ändern. Bei vernünftigen Implementierungen sollten jedoch nur die Klassen zwischen der aktuellen Klasse und der angeforderten Klasse überprüft werden. Es ist also einfach linear in der Anzahl der Klassen zwischen den beiden, nicht linear in der Anzahl der Klassen in der Hierarchie.

Typ von

Dies bedeutet, dass der Operator typeof verwendet wird, um das Objekt std::type_info abzurufen, das einem Objekt zugeordnet ist.

Die Leistung ist invariant mit der Größe der Hierarchie. Wenn die Klasse eine virtuelle Klasse ist (virtuelle Funktionen oder virtuelle Basisklassen), wird sie einfach aus der vtable herausgezogen. Wenn es nicht virtuell ist, ist die Kompilierzeit definiert.

Fazit

Kurz gesagt, die meisten Operationen sind invariant mit der Größe der Hierarchie. Aber selbst in den Fällen, in denen es Auswirkungen hat, ist das kein Problem.

Ich würde mich mehr mit einer Designethik befassen, in der Sie das Bedürfnis verspürten, eine solche Hierarchie aufzubauen. Meiner Erfahrung nach stammen solche Hierarchien aus zwei Designlinien.

  1. Das Ideal von Java / C #, dass alles von einer gemeinsamen Basisklasse abgeleitet wird. Dies ist eine schreckliche Idee in C ++ und sollte nie verwendet werden. Jedes Objekt sollte von dem, was es braucht, und nur davon abgeleitet werden. C ++ wurde auf dem Prinzip "Pay for which you use" aufgebaut, und das Ableiten von einer gemeinsamen Basis funktioniert dagegen. Im Allgemeinen ist alles, was Sie mit einer solchen gemeinsamen Basisklasse tun können, entweder etwas, das Sie nicht tun sollten, oder etwas, das mit Funktionsüberladung durchgeführt werden könnte (indem Sie operator<< zum Beispiel in Strings konvertieren).

  2. Missbrauch der Vererbung. Verwenden der Vererbung, wenn Sie Containment verwenden sollten. Vererbung erstellt eine "ist ein" Beziehung zwischen Objekten. Meistens haben "hat" Beziehungen (ein Objekt, das ein anderes als Mitglied hat) viel nützlicher und flexibler. Sie erleichtern das Ausblenden von Daten und erlauben dem Benutzer nicht, eine Klasse als eine andere darzustellen.

Stellen Sie sicher, dass Ihr Design nicht gegen eines dieser Prinzipien verstößt.

    
Nicol Bolas 05.11.2011, 07:10
quelle
2

Es wird aber nicht so schlimm wie die Programmiererleistung sein.

    
Tom 05.11.2011 06:41
quelle
0

Wie @Nicol hervorhebt, kann es eine Reihe von Dingen tun. Wenn dies Dinge sind, die Sie tun müssen, unabhängig vom Design, weil sie alle genau notwendige Schritte sind, um das Programm von call main auf exit innerhalb der wenigsten möglichen Zyklen zu bekommen, dann ist Ihr Design einfach eine Angelegenheit der Klarheit der Codierung (oder vielleicht fehlt es:).

Nach meiner Erfahrung Performance-Tuning wie in diesem Beispiel Was ich oft als eine große Quelle von verschwendeter Zeit sehe, ist das Überdesign von Datenstrukturen (dh Klassen). Sonderbarerweise ist die Rechtfertigung für die Datenstrukturen oft (rate was?) - Leistung!

Nach meiner Erfahrung ist die Datenstruktur so einfach wie möglich und möglichst normalisiert. Wenn es vollständig normalisiert ist, kann jede einzelne Änderung es nicht inkonsistent machen. Sie können nicht immer vollständige Normalität erreichen, in diesem Fall müssen Sie mit der Möglichkeit umgehen, dass die Daten vorübergehend inkonsistent sein können. Deshalb schreiben Leute Benachrichtigungshandler , und dies wird in OOP empfohlen. Die Idee ist, wenn Sie etwas an einem Ort ändern, dass Benachrichtigungen ausgelöst werden können, die "automatisch" die Änderung an andere Orte propagieren und versuchen, die Konsistenz zu erhalten.

Das Problem mit Benachrichtigungen ist, dass sie weglaufen können. Das Ändern einiger boolescher Eigenschaften von true in false kann einen Feuersturm von Benachrichtigungen verursachen, die durch die Datenstruktur auf eine Art und Weise rauschen, die kein Programmierer versteht, Datenbanken aktualisiert, Fenster malt, Dateien zippt usw. usw. Ich finde das oft ist, wo die meisten Taktzyklen gehen.

Ich denke, es ist einfacher und viel effizienter, Inkonsistenzen zeitweise zu tolerieren und sie regelmäßig mit einem umfassenden Prozess zu reparieren.

Eine andere Möglichkeit, mit der Datenstrukturen mit großer Ineffizienz einhergehen, ist die Tatsache, dass die Daten von irgendeinem Prozess effektiv interpretiert werden, um eine Ausgabe zu erzeugen. Dies ist sehr häufig in Grafiken. Wenn sich die Daten sehr langsam ändern, kann es sinnvoll sein, sie "zu kompilieren", anstatt sie "zu interpretieren". Mit anderen Worten übersetzt man es in einen einfacheren Befehlssatz oder Quellcode, der "on the fly" kompiliert wird, der dann viel schneller ausgeführt werden kann, um die gewünschte Ausgabe zu erzeugen.

    
Mike Dunlavey 05.11.2011 19:44
quelle

Tags und Links