Kann ich das Curiously Recurring Template Pattern hier (C ++) verwenden?

7

Ich habe eine C ++ - Anwendung, die zu etwas wie diesem vereinfacht werden kann:

%Vor%

Meine Anwendung ist leistungskritisch. In der Sammlung befinden sich normalerweise Tausende von Widgets. Die von AbstractWidget abgeleiteten Klassen (von denen es Dutzende gibt) lassen in der Regel viele der virtuellen Funktionen nicht außer Kraft. Diejenigen, die überschrieben werden, haben in der Regel sehr schnelle Implementierungen.

Angesichts dessen denke ich, dass ich mein System mit einer cleveren Metaprogrammierung optimieren kann. Das Ziel besteht darin, Funktionsinlining zu nutzen und virtuelle Funktionsaufrufe zu vermeiden, während der Code managbar bleibt. Ich habe mir das Muster "Neugierig wiederkehrendes Template" angesehen (siehe hier für eine Beschreibung). Das scheint fast zu tun, was ich will, aber nicht ganz.

Gibt es irgendeine Möglichkeit, das CRTP hier für mich arbeiten zu lassen? Oder gibt es eine andere clevere Lösung, an die jeder denken kann?

    
HostileFork 16.06.2009, 16:27
quelle

6 Antworten

4

CRTP oder Kompilierzeit-Polymorphie ist für, wenn Sie alle Ihre Typen zur Kompilierzeit kennen. Solange Sie addWidget verwenden, um eine Liste von Widgets zur Laufzeit zu sammeln und solange fooAll und barAll , dann müssen Sie zur Laufzeit mit Mitgliedern dieser homogenen Liste von Widgets umgehen können verschiedene Typen zur Laufzeit. Also für das Problem, das Sie vorgestellt haben, glaube ich, dass Sie mit Laufzeit-Polymorphismus stecken.

Eine Standardantwort besteht natürlich darin, zu verifizieren, dass die Leistung des Laufzeitpolymorphismus ein Problem ist, bevor Sie versuchen, es zu vermeiden ...

Wenn Sie den Laufzeitpolymorphismus wirklich vermeiden müssen, kann eine der folgenden Lösungen funktionieren.

Option 1: Verwenden Sie eine Sammlung von Widgets zur Kompilierungszeit

Wenn die Mitglieder Ihrer WidgetCollection zum Zeitpunkt der Kompilierung bekannt sind, können Sie sehr einfach Vorlagen verwenden.

%Vor%

Option 2: Ersetzen Sie den Laufzeitpolymorphismus durch freie Funktionen

%Vor%

Hässlich, und wirklich nicht OO. Vorlagen könnten dabei helfen, die Notwendigkeit zu reduzieren, jeden Spezialfall aufzulisten. versuche etwas wie das Folgende (vollständig ungetestet), aber du bist zurück zu keinem Inlining in diesem Fall.

%Vor%

Option 3: OO eliminieren

OO ist nützlich, weil es hilft, die Komplexität zu verwalten, und weil es dazu beiträgt, die Stabilität angesichts von Änderungen aufrechtzuerhalten. Für die Umstände, die Sie zu beschreiben scheinen - Tausende von Widgets, deren Verhalten sich im Allgemeinen nicht ändert und deren Mitgliedermethoden sehr einfach sind - haben Sie möglicherweise nicht viel Komplexität oder Änderungen zu verwalten. Wenn dies der Fall ist, brauchen Sie möglicherweise OO nicht.

Diese Lösung entspricht dem Laufzeitpolymorphismus, außer dass Sie eine statische Liste mit "virtuellen" Methoden und bekannten Unterklassen (die nicht OO ist) verwalten müssen und virtuelle Funktionsaufrufe durch eine Jump-Tabelle in inlined ersetzen können Funktionen.

%Vor%     
Josh Kelley 16.06.2009, 17:00
quelle
7

Simulierte dynamische Bindung (es gibt andere Verwendungen von CRTP) ist für, wenn die Basisklasse sich selbst als polymorph betrachtet, aber Clients nur eine bestimmte abgeleitet ist Klasse. Zum Beispiel könnten Sie Klassen haben, die eine Schnittstelle zu einigen plattformspezifischen Funktionen darstellen, und jede gegebene Plattform wird immer nur eine Implementierung benötigen. Der Punkt des Musters besteht darin, die Basisklasse zu templatisieren, so dass, obwohl es mehrere abgeleitete Klassen gibt, die Basisklasse zur Kompilierungszeit weiß, welche verwendet wird.

Es hilft Ihnen nicht, wenn Sie wirklich Laufzeit-Polymorphismus benötigen, wie zum Beispiel, wenn Sie einen Container von AbstractWidget* haben, jedes Element kann eine von mehreren abgeleiteten Klassen sein, und Sie müssen darüber iterieren. In CRTP (oder einem anderen Vorlagencode) sind base<derived1> und base<derived2> nicht verwandte Klassen. Dasselbe gilt für derived1 und derived2 . Es gibt keinen dynamischen Polymorphismus zwischen ihnen, es sei denn, sie haben eine andere gemeinsame Basisklasse, aber dann sind Sie wieder da, wo Sie mit virtuellen Anrufen begonnen haben.

Sie können eine Beschleunigung erzielen, indem Sie Ihren Vektor durch mehrere Vektoren ersetzen: einen für jede abgeleitete Klasse, den Sie kennen, und einen generischen, wenn Sie später neue abgeleitete Klassen hinzufügen und den Container nicht aktualisieren. Dann fügt addWidget eine (langsame) typeid Prüfung oder einen virtuellen Aufruf zum Widget hinzu, um das Widget dem richtigen Container hinzuzufügen, und hat möglicherweise Überladungen, wenn der Aufrufer die Laufzeitklasse kennt. Achten Sie darauf, nicht versehentlich eine Unterklasse von WidgetIKnowAbout zum Vektor WidgetIKnowAbout* hinzuzufügen. fooAll und barAll können wiederum über jeden Container laufen und (schnell) Aufrufe an nicht-virtuelle fooImpl und barImpl Funktionen machen, die dann inline werden. Sie durchlaufen dann den hoffentlich viel kleineren Vektor AbstractWidget* und rufen die virtuellen Funktionen foo oder bar auf.

Es ist ein bisschen unordentlich und nicht pure-OO, aber wenn fast alle Ihre Widgets zu Klassen gehören, die Ihr Container kennt, dann sehen Sie möglicherweise eine Leistungssteigerung.

Beachten Sie, dass, wenn die meisten Widgets zu Klassen gehören, über die Ihr Container möglicherweise nichts weiß (weil sie sich beispielsweise in verschiedenen Bibliotheken befinden), Sie möglicherweise keine Inlining-Funktion haben (sofern Ihr dynamischer Linker nicht inline ist. t). Sie könnten den virtuellen Anruf-Overhead fallen lassen, indem Sie sich mit Mitgliedsfunktionszeigern herumärgern, aber der Gewinn wäre mit ziemlicher Sicherheit vernachlässigbar oder sogar negativ. Der größte Teil des Overheads eines virtuellen Aufrufs liegt im Aufruf selbst, nicht im virtuellen Lookup, und Aufrufe durch Funktionszeiger sind nicht inline.

Sehen Sie es sich anders an: Wenn der Code inline sein soll, bedeutet dies, dass der tatsächliche Maschinencode für die verschiedenen Typen unterschiedlich sein muss. Dies bedeutet, dass Sie entweder mehrere Schleifen oder eine Schleife mit einem Schalter benötigen, da der Maschinencode bei jedem Durchgang durch die Schleife nicht im ROM geändert werden kann, je nachdem, welcher Zeiger aus einer Sammlung gezogen wurde. p>

Nun, ich schätze, vielleicht könnte das Objekt etwas Asm-Code enthalten, den die Schleife in den RAM kopiert, ausführbar markiert und hineinspringt. Aber das ist keine C ++ - Memberfunktion. Und es kann nicht portabel gemacht werden. Und es wäre wahrscheinlich nicht einmal schnell, was mit dem Kopieren und der Icache-Entwertung. Deshalb gibt es virtuelle Anrufe ...

    
Steve Jessop 16.06.2009 17:05
quelle
4

Die kurze Antwort ist nein.

Die lange Antwort (oder noch kurz mit einigen anderen Antworten: -)

Sie versuchen dynamisch herauszufinden, welche Funktion zur Laufzeit ausgeführt werden soll (das sind die virtuellen Funktionen). Wenn Sie einen Vektor haben (dessen Mitglieder zum Zeitpunkt der Kompilierung nicht bestimmt werden können), können Sie nicht herausfinden, wie Sie die Funktionen zusammenfassen können, egal, was Sie versuchen.

Das einzige Problem ist, dass wenn die Vektoren immer dieselben Elemente enthalten (dh Sie könnten eine Kompilierzeit ausarbeiten, was zur Laufzeit ausgeführt wird). Sie könnten dies dann erneut bearbeiten, aber es würde etwas anderes als ein Vektor erfordern, um die Elemente zu halten (wahrscheinlich eine Struktur mit allen Elementen als Mitglieder).

Glauben Sie wirklich, dass der virtuelle Versand ein Flaschenhals ist? Persönlich bezweifle ich es sehr.

    
Martin York 16.06.2009 17:39
quelle
3

Das Problem, das Sie hier haben werden, ist mit WidgetCollection::widgets . Ein Vektor kann nur Elemente eines Typs enthalten, und die Verwendung des CRTP erfordert, dass jedes AbstractWidget einen anderen Typ hat, der durch den gewünschten abgeleiteten Typ templatisiert wird. Das heißt, Sie sehen AbstractWidget in etwa so aus:

%Vor%

Dies bedeutet, dass jedes AbstractWidget mit einem anderen Derived Typ einen anderen Typ AbstractWidget< Derived > darstellt. Das alles in einem Vektor zu speichern funktioniert nicht. Es sieht also so aus, als wären virtuelle Funktionen der richtige Weg.

    
Naaff 16.06.2009 16:56
quelle
3

Nicht, wenn Sie einen Vektor von ihnen brauchen. Die STL-Container sind vollständig homogen. Wenn Sie also ein WidgetA und ein WidgetB in demselben Container speichern müssen, müssen sie von einem gemeinsamen übergeordneten Objekt geerbt werden. Und wenn widgetA :: bar () etwas anderes als widgetB :: bar () tut, müssen Sie die Funktionen virtuell machen.

Sollen alle Widgets im selben Container sein? Sie könnten etwas wie

tun %Vor%

Und dann müssten die Funktionen nicht virtuell sein.

    
rlbond 16.06.2009 16:57
quelle
1

Die Chancen stehen gut, dass du, nachdem du diese ganze Anstrengung durchgespielt hast, keinen Leistungsunterschied sehen wirst.

Dies ist absolut die falsche Art zu optimieren. Sie würden keinen logischen Fehler beheben, indem Sie zufällige Codezeilen ändern, oder? Nein, das ist albern. Sie "reparieren" den Code nicht, bis Sie zuerst herausfinden, welche Zeilen tatsächlich Ihr Problem verursachen. Warum würden Sie Performance Bugs anders behandeln?

Sie müssen Ihre Anwendung profilieren und herausfinden, wo die echten Engpässe liegen. Beschleunigen Sie dann diesen Code und führen Sie den Profiler erneut aus. Wiederholen Sie den Vorgang, bis der Leistungsfehler (zu langsame Ausführung) behoben ist.

    
T.E.D. 16.06.2009 17:48
quelle