Frage an alle die denken, dynamisches casting ist ein Zeichen von schlechtem Softwaredesign



  • mgaeckler schrieb:

    volkard schrieb:

    Wenn dynamic_cast klappt, hast Du eh die vtbl im code und den vptr in jedem Objekt. Da wird durch eine weitere virtuelle Funktion nichts langsamer.

    Doch. Die Programme, die diese Bibliothek benutzen werden größer und zwar alle unabhängig davon, ob sie die Funktionen alle nutzen oder nicht. Das macht die Programme daher zwangsweise langsamer. Ob sich das bemerkbar macht oder nicht, hängt natürlich vom Einzelfall ab. In einem Fall ging es um eine Lasttestanwendung, die möglichst viel Last auf einem Server generieren sollte ohne aber selbst unnötig Overhead zu erzeugen.
    mfg Martin

    Tja, ohne dynamic_cast hättest Du die rtti-Tables nicht gebaraucht und vielleicht so mehr Platz gespart. Und dynamic_cast kann extram langsam sein. 1k RAM mehr zu benötigen ist keine Schande mehr. Da müßte man schon extrem genau messen können, um einen Effekt auf die Laufzeit nachzuweisen. Und wo soll das hinführen. Verzicht auf die Benutzung von std::string, weil es Anwender geben könnte, die selber kein std::string brauchen und deswegen dann eine kleinere ausführbare Datei hätten?

    Die Entscheidung von Dir scheint eine Überlegung zu sein, wo das letzte Promille Speed herausgequetscht werden soll. Dann würde ich mal schauen, ob man schummeln kann. Zum Beispiel

    #ifdef MSVC
    unsigned long ::getTypeID(IBase* self){return *reinterpret_cast<unsigned int*>(self);}
    //liest einfach den vptr aus. 
    //zerfällt je nach dem zu einem schnellen asm-befehl 
    //oder zu nur einer anderen adressierung eines anderen befehls. 
    #endif
    

    edit: Freie Funktion aus der Methode gemacht. WIllst ja nicht in die Klasse eingreifen müssen.



  • Nexus schrieb:

    mgaeckler schrieb:

    Doch. Die Programme, die diese Bibliothek benutzen werden größer

    Was wird grösser? Der zusätzliche VTable-Eintrag?

    Die 4 oder 8 Extrabyte der VTable sind natürlich irrelevant. Aber es müssen ja dann auch alle Funktionen dazugebunden werden. Das sind dann schon deutlich mehr als 4 Byte.

    Nexus schrieb:

    mgaeckler schrieb:

    Das macht die Programme daher zwangsweise langsamer.

    Aber daran, dass dynamic_cast in vielen Implementierungen sehr langsam ist, denkst du nicht.

    Doch natürlich. Nur: Die Anwendung, die den dynamic cast benutzt hat, war ein simpler XML-Editor. Die Reaktionszeit war immer noch kürzer als ein Augenschlag des Anwenders dauert, trotz des furchtbaren dynamic casts.

    Nexus schrieb:

    mgaeckler schrieb:

    Ob sich das bemerkbar macht oder nicht, hängt natürlich vom Einzelfall ab. In einem Fall ging es um eine Lasttestanwendung, die möglichst viel Last auf einem Server generieren sollte ohne aber selbst unnötig Overhead zu erzeugen.

    Ja, und genau deshalb solltest du messen. Erst recht, wenn es um eine zeitkritische Anwendung geht. Bitte meide nicht virtuelle Funktionen und weiche auf fragwürdige Alternativen wie dynamic_cast aus, weil du das Gefühl hast, virtuelle Funktionen seien langsam.

    Ich habe nie geschrieben, daß virtuelle Funktionen langsam sind. Ich habe geschrieben, daß sie Overhead erzeugen, der manchmal nicht aktzeptabel ist und ich habe auch ein Beispiel genannt.

    Der Lasttest ist in der Lage, das Verhalten von 1000 Benutzern und mehr zu simulieren. Dazu stehen dann aber natürlich nicht 1000 Rechner zu Verfügung. Der Lasttest darf dann natürlich nicht selbst zum Flaschenhals werden, sonst misst man/frau an der falschen Stelle.

    mfg Martin



  • mgaeckler schrieb:

    Die 4 oder 8 Extrabyte der VTable sind natürlich irrelevant. Aber es müssen ja dann auch alle Funktionen dazugebunden werden. Das sind dann schon deutlich mehr als 4 Byte.

    Nicht benötigte Funktionen müssen nicht zwingend gelinkt werden. Das hängt von den Optimierungsmöglichkeiten des Compilers ab. Aber selbst wenn: Glaubst du, dynamic_cast brauche keinen Code für die Logik des Typ-Bestimmens zur Laufzeit?

    mgaeckler schrieb:

    Doch natürlich. Nur: Die Anwendung, die den dynamic cast benutzt hat, war ein simpler XML-Editor. Die Reaktionszeit war immer noch kürzer als ein Augenschlag des Anwenders dauert, trotz des furchtbaren dynamic casts.

    Aber mit dem Overhead der virtuellen Funktion hätte es knapp nicht mehr gereicht, verstehe.

    mgaeckler schrieb:

    Ich habe nie geschrieben, daß virtuelle Funktionen langsam sind. Ich habe geschrieben, daß sie Overhead erzeugen, der manchmal nicht aktzeptabel ist und ich habe auch ein Beispiel genannt.

    Natürlich sind virtuelle Funktionen manchmal unangebracht. Deswegen sind Methoden in C++ standardmässig auch nicht virtuell. Nur ist es etwas fragwürdig, durch eine Annahme bezüglich des Overheads zu einem anderen Sprachmittel zu greifen, das nicht gerade wenige Nachteile hat.

    Wenn du nach ausführlichem Profiling zum Schluss gekommen wärst, dass dynamic_cast tatsächlich die bessere Lösung sei, und dass die Umstellung überhaupt relevanten Einfluss auf die Gesamt-Performance hat, dann okay. Aber so wie du es bisher beschrieben hast, scheinen deine Design-Entscheidungen eher auf Annahmen und Überlegungen zu basieren. Und diese können oft falsch sein, weil man die genaue Implementierung nicht kennt, weil etliche Faktoren dazukommen, oder weil man sich generell schnell verschätzen kann.



  • Wie ich das verstanden habe:
    Es gibt eine schlanke XML-Bibliothek, wo diese Funktionalität fehlt.

    Das Programm Lasttest benutzt die Bibliothek und freut sich über das eingesparte Kilobyte. Lasttest soll so schnell wie möglich sein. Lasttest braucht die Funktionalität nicht.

    Das Programm XML-Editor braucht die Funktionalität und nimmt einfach dynamic_cast. Der XML-Editor kann beliebig inperformant sein.

    Es bleibt, daß ich auch hier dynamic_cast für schlecht halte und lieber die Bibliothek aufmotzen würde. Falls sie aus dem eigenen Hause stammt oder ich sonst einfachen Einfluß darauf habe.

    Und es bleibt

    die Aussage, dynamisches casten sei ein Zeichen schlechten Softwaredesigns, ist völliger Humbug.

    kann man nicht so stehen lassen.



  • Nexus schrieb:

    Wenn du nach ausführlichem Profiling zum Schluss gekommen wärst, dass dynamic_cast tatsächlich die bessere Lösung sei, und dass die Umstellung überhaupt relevanten Einfluss auf die Gesamt-Performance hat, dann okay. Aber so wie du es bisher beschrieben hast, scheinen deine Design-Entscheidungen eher auf Annahmen und Überlegungen zu basieren. Und diese können oft falsch sein, weil man die genaue Implementierung nicht kennt, weil etliche Faktoren dazukommen, oder weil man sich generell schnell verschätzen kann.

    Ich merke schon. Wir reden aneinander vorbei.

    Ich habe eine Bibliothek zur XML Verarbeitung. Die Bibliothek wird von mehreren Anwendungen benutzt.

    Eine dieser Anwendung ist ein simpler XML Editor. Der hat performance mässig kein Problem, der darf den dynamic cast benutzen, weil der Bibliothek eine Eigenschaft nicht liefert.

    Eine andere Anwendung, die mit dem XML-Editor nur gemeinsam hat, daß sie auch die Bibliothek benutzt, ist ein Lasttestprogramm. Diese darf selber kein Flaschenhals sein. Die Bibliothek wird daher nur allgemeine Eigenschaften bzw. Fähigkeiten zur Verfügung stellen, die von allgemeiner Bedeutung sind. Irgendwelche Spezialfälle, die nur für den XML Editor wichtig sind, der keine Performanceprobleme hat, kommen daher nicht in die Bibliothek. Natürlich benutzt auch die XML-Lib virtuelle Funktionen, wo immer es erforderlich ist.

    Und, das Lasttestprogramm selber wurde natürlich einem genauen Profiling unterworfen, damit es eben kein Flaschenhals ist.

    mfg Martin



  • Ah, okay. Ob es sich bei der fehlenden Methode wirklich um einen Designfehler in der XML-Bibliothek handelt, hängt natürlich davon ab, wie die Bibliothek im Allgemeinen benutzt werden soll. Also nicht nur auf den Editor bezogen.

    Denn wäre es wirklich nur ein editor-spezifisches Problem, das dynamic_cast erfordert, so ist es gut möglich, dass entsprechender Clientcode im Editor eine andere Lösung als dynamic_cast findet. Aber ich kenne das konkrete Design zu wenig.

    Schlussendlich ist es auch nicht tragisch, dynamic_cast einmal einzusetzen. Nur sollte man sich dessen Nachteile bewusst sein (und Performance ist oft nicht der entscheidende Nachteil), und allfällige Alternativen in Erwägung ziehen.



  • volkard schrieb:

    Wie ich das verstanden habe:
    Es gibt eine schlanke XML-Bibliothek, wo diese Funktionalität fehlt.

    Das Programm Lasttest benutzt die Bibliothek und freut sich über das eingesparte Kilobyte. Lasttest soll so schnell wie möglich sein. Lasttest braucht die Funktionalität nicht.

    Das Programm XML-Editor braucht die Funktionalität und nimmt einfach dynamic_cast. Der XML-Editor kann beliebig inperformant sein.

    So ähnlich kannst Du es formulieren. "beliebig inperformant" ist allerdings etwas übertrieben. 🙂

    volkard schrieb:

    Es bleibt, daß ich auch hier dynamic_cast für schlecht halte und lieber die Bibliothek aufmotzen würde. Falls sie aus dem eigenen Hause stammt oder ich sonst einfachen Einfluß darauf habe.

    Der XML-Standard ist nicht sehr volatil. Und gerade weil alles aus einer Hand stammt, kann ich sehr gut auf Veränderungen reagieren. Aber eines ist mir natürlich bewusst, wenn ich die Bibliothek veröffentlichen würde, wären natürlich andere Anforderungen an diese zu stellen und die Effizienz müsste darunter "leiden", da in diesem Falle natürlich die Abstraktion auf ein wesentlich höheres Niveau steigen muß als es jetzt ist.

    volkard schrieb:

    Und es bleibt

    die Aussage, dynamisches casten sei ein Zeichen schlechten Softwaredesigns, ist völliger Humbug.

    kann man nicht so stehen lassen.

    Ich seh ein, da habe wohl ich übertrieben.

    mfg Martin



  • Hei Leut, mal wieder zurück zum Thema! :p

    Also ich habe das Ganze mittlerweile mit dem double dispatcher Pattern implementiert und funktioniert auch ganz dolle. Nun kommt aber gleich wieder ein Problem und zwar muss ich die BoundingVolumeHierarchie rendern (zu Debugginzwecken). Der Renderer muss also IMHO die BoundingVolumes kennen, umgekehrt aber nicht. Nun ist es so, dass bspw. eine BoundingSphere vollständig anders gerendert wird, als eine kDOP oder eine OOB. Ausserdem haben diese natürlich auch ganz andere Membervariablen. Für die Physikengine ist es gut gelöst, die kommuniziert nämlich ausschliesslich im der Abstrakten Klasse BoundingVolume und muss nichts über die interne Geometry wissen. Die will nur wissen, hat eine Kollision stattgefunden und falls ja an welcher Stelle im kartesichen Raum. Der Renderer hingegen muss nun sehr genau wissen, wie die Geometry des entsprechenden Proxykörpers aufgebaut ist. Da aber eine BVH ein Baum von BoundingVolumes ist frage ich mich, wie ich dies nun ohne dynamic_cast lösen kann?



  • Warum nicht? Die virtuelle Funktion im Objekt weiß doch, welches BoundingVolume passend ist.



  • Ja aber nicht welches Mesh für das Rendern verwendet werden soll, da die BoundingVolume nichts mit Grafik am Hut hat. Ausserdem werden OBB's als Indexed line list, kDops als indexed Trianglelists und BoundingSpheres als vorgeladenes Mesh gerendert. Alles Informationen, über welche die BoundingVolume nicht verfügt, der Renderer hingegen schon. Dafür weiss der Renderer ohne dynamic_cast nicht, wie die Paramater der jeweiligen geometrischen Form aussehen.

    So nun bin ich gespannt! 😉



  • Wie siehts mit dem Visitorpattern aus?

    IVolume* vol;
    Renderer r;
    vol->accept(r); // vol ruft dann die passende Methode im Renderer auf
    

    Sozusagen SingleDispatching anders rum



  • Dafür muss das Volume den Renderer kennen und das ist überhaupt nicht in meinem Sinne (Package Dependency, violation of Layer contraints)! Also eine Abhängigkeit von der Mathlibrary zur Renderlibrary? Mit anderen Worten, ich kann die Mathlibrary in anderen Projekten nur dann benutzen, wenn ich ebenfalls die Renderlibrary hinzunehme? Nein Danke! :p



  • Findet ihr, dass double dispatching wirklich schönes Design ist? Ich finde, dass das nur eine ganze Menge schreibarbeit ist, vorallem, wenn man ca. 10 verschiedene Objekttypen hat.

    Was macht man z.B. wenn man eine fertige 3rd Party Lib ohne Source hat, die Kollisionserkennung für ein paar Grundtypen hat und man will noch User-Definierte-Objekte zulassen? Da kann ich ja schlecht mit double dispatching arbeiten.



  • @mal ehrlich
    Meine Rede! 🙂



  • mal ehrlich schrieb:

    Findet ihr, dass double dispatching wirklich schönes Design ist? Ich finde, dass das nur eine ganze Menge schreibarbeit ist, vorallem, wenn man ca. 10 verschiedene Objekttypen hat.

    Double Dispatching ist unschön und führt zu viel Boilerplate-Code, wenn man es intrusiv implementiert. Deshalb gibt es Leute wie Andrei Alexandrescu, die sich elegantere Möglichkeiten ausgedacht haben. 🙂

    Hier ein Beispiel der Verwendung von Double-Dispatch mit der Loki-Bibliothek:

    #include <Loki/MultiMethods.h>
    #include <iostream>
    
    struct Object
    {
    	virtual ~Object() = 0 {}
    };
    
    struct Asteroid : Object
    {
    };
    
    struct SpaceShip : Object
    {
    };
    
    void Collision(Asteroid&, Asteroid&)
    {
    	std::cout << " Asteroid <-> Asteroid" << std::endl;
    }
    
    void Collision(Asteroid&, SpaceShip&)
    {
    	std::cout << " Asteroid <-> SpaceShip" << std::endl;
    }
    
    void Collision(SpaceShip&, SpaceShip&)
    {
    	std::cout << "SpaceShip <-> SpaceShip" << std::endl;
    }
    
    int main()
    {
    	// Dispatch-Objekt erstellen; Object = Basisklassentyp
    	Loki::FnDispatcher<Object> disp;
    
    	// Einzelne Funktionen registrieren; sym=symmetrisch bedeutet Fn(a,b) == Fn(b,a)
    	//         LHS       RHS      Funktion   sym?
    	disp.Add<Asteroid, Asteroid, &Collision>();
    	disp.Add<Asteroid, SpaceShip, &Collision, true>();
    	disp.Add<SpaceShip, SpaceShip, &Collision>();
    
    	// Referenzen auf abstrakte Basisklasse erstellen
    	Asteroid a;
    	SpaceShip s;
    	Object& pa = a;
    	Object& ps = s;
    
    	// Multimethoden aufrufen
    	disp.Go(a, a);
    	disp.Go(a, s);
    	disp.Go(s, a); // <- Symmetrie!
    	disp.Go(s, s);
    }
    


  • Grüße!

    Also dynamic_cast an sich ist kein Zeichen für schlechtes Design. Wer das behauptet übertreibt ein wenig. Und weder Schneeweiß, noch Alexandrescu noch Meyers behaupten so etwas. Im zitierten Buch von Scott Meyers steht drin, ich habe es gerade nachgeschlagen, dass es ein Indiz für ein fehlerhaftes Design ist, wenn man sich gezwungen sieht eine dynamic_cast-Kaskade schreiben zu müssen. (Heißt, jede Menge if-else-if Tests mit dynamic_cast auf deine Objekte.)

    dynamic_cast an sich ist zwar "teuer" im Sinne von "Kostet Zeit", aber wenn der Nutzen die Kosten überwiegt, bitte. (Bei einer Physik-Engine glaube ich da aber nicht so recht dran.)

    Eigentlich stellt sich zuerst die Frage, wie du das Problem gelöst haben möchtest. Zur Kompilierzeit? Dann wäre vielleicht ein TMP-Ansatz, wie zum Beispiel über <tr1/type_traits>, möglich? Oder wird deine Engine keine zeitkritischen Einsätze haben, so dass eine Laufzeitlösung über RTTI, wie zum Beispiel mit <typeinfo>, in Betracht käme?

    Bei nur zwei Klassen, würde ich eine Lösung mit typeinfo und einem static_cast einer Prüfung durch dynamic_cast vorziehen.



  • Nexus schrieb:

    mal ehrlich schrieb:

    Findet ihr, dass double dispatching wirklich schönes Design ist? Ich finde, dass das nur eine ganze Menge schreibarbeit ist, vorallem, wenn man ca. 10 verschiedene Objekttypen hat.

    Double Dispatching ist unschön und führt zu viel Boilerplate-Code, wenn man es intrusiv implementiert. Deshalb gibt es Leute wie Andrei Alexandrescu, die sich elegantere Möglichkeiten ausgedacht haben. 🙂

    Hier ein Beispiel der Verwendung von Double-Dispatch mit der Loki-Bibliothek:

    #include <Loki/MultiMethods.h>
    #include <iostream>
    
    struct Object
    {
    	virtual ~Object() = 0 {}
    };
    
    struct Asteroid : Object
    {
    };
    
    struct SpaceShip : Object
    {
    };
    
    void Collision(Asteroid&, Asteroid&)
    {
    	std::cout << " Asteroid <-> Asteroid" << std::endl;
    }
    
    void Collision(Asteroid&, SpaceShip&)
    {
    	std::cout << " Asteroid <-> SpaceShip" << std::endl;
    }
    
    void Collision(SpaceShip&, SpaceShip&)
    {
    	std::cout << "SpaceShip <-> SpaceShip" << std::endl;
    }
    
    int main()
    {
    	// Dispatch-Objekt erstellen; Object = Basisklassentyp
    	Loki::FnDispatcher<Object> disp;
    
    	// Einzelne Funktionen registrieren; sym=symmetrisch bedeutet Fn(a,b) == Fn(b,a)
    	//         LHS       RHS      Funktion   sym?
    	disp.Add<Asteroid, Asteroid, &Collision>();
    	disp.Add<Asteroid, SpaceShip, &Collision, true>();
    	disp.Add<SpaceShip, SpaceShip, &Collision>();
    	
    	// Referenzen auf abstrakte Basisklasse erstellen
    	Asteroid a;
    	SpaceShip s;
    	Object& pa = a;
    	Object& ps = s;
    	
    	// Multimethoden aufrufen
    	disp.Go(a, a);
    	disp.Go(a, s);
    	disp.Go(s, a); // <- Symmetrie!
    	disp.Go(s, s);
    }
    

    Dann schau mal den Code dazu an:
    http://loki-lib.sourceforge.net/html/a00658.html
    Da wird typeid, callbackMaps, DynamicCaster usw. verwendet. Im Prinzip so ähnlich, wie ich es auch gemacht hätte, wenn ich was erweiterbares brauchen würde, wenn ich auch nicht so viel mit Templates gemacht hätte. Mit double dispatching hat das eigentlich nix mehr zu tun.



  • mal ehrlich schrieb:

    Mit double dispatching hat das eigentlich nix mehr zu tun.

    Doch. Nur musst du dann den Begriff etwas weiter fassen als nur die eine konkrete Implementierung über gegenseitige virtuelle Methodenaufrufe im Visitor-Stil. Die ist nämlich wie schon angetönt für viele Fälle zu unflexibel und schlecht erweiterbar.

    Ich verstehe unter Double-Dispatching die Möglichkeit, aufgrund zweier dynamischer Typen zur Laufzeit die richtige Funktion aufzurufen. Die konkrete Implementierung ist dabei hintergründig, im Zentrum steht die Anwendung. Es gibt Sprachen, die haben dieses Feature bereits eingebaut.

    Immer noch kein Grund, einen Full-Quote zu machen. 😉



  • Ich hab seinen Code jetzt nur überflogen, aber eigentlich hat es sowas gemacht.

    map< pair<ClassID1, ClassID2>, callbackFunc >
    
    go(class1, class2)
    {
        callbackFunc = map.find(class1.ID, class2.ID)
        callbackFunc(class1, class2)
    }
    

    Das wird doch keiner als double dispatch bezeichnen.



  • mal ehrlich schrieb:

    Das wird doch keiner als double dispatch bezeichnen.

    Wie schon erwähnt: Warum interessiert dich die Implementierung? Für mich ist Double-Dispatch ein Konzept und keine konkrete Realisierung davon. Du klammerst dich an den einen speziellen Fall fest.

    Ishildur geht es darum, abhängig von zwei dynamischen Typen eine jeweils andere Funktion aufzurufen. Also im Prinzip Funktionsüberladung, aber eben nicht mit statischen Typen, sondern zur Laufzeit. Und dafür gibt es mehrere Möglichkeiten, wobei ich Alexancrescus Ansatz sehr elegant finde, hauptsächlich aus den folgenden Gründen:

    • Kein intrusiver Code: Man kann Klassen unverändert lassen. Die Verwendung symmetrischer freier Funktionen anstelle virtueller Methoden hat ebenfalls Vorteile.
    • Erweiterbarkeit: Man kann ohne viel Aufwand neue Multimethoden hinzufügen, die automatisch berücksichtigt werden.

Anmelden zum Antworten