Wie internen Zustand von Objekt auslagern?



  • Ich hatte diese Frage schon einmal gestellt. Und ich glaube, dass es eventuell mehr in RudP gehört, da es ein allgemeines Problem ist. Das letzte mal ist es nicht besonders toll gelaufen, weil ich den Fehler gemacht habe, ein Beispiel zu liefern und die Problemlösungen auf dieses spezielle Beispiel abgehoben sind und leider nur versuchten, das Problem auf Grundlage dieses einen Beispiels zu transformieren, anstatt es zu lösen "das kannst du dort ganz anders machen". Diesen Fehler mache ich jetzt nicht. hah!

    Ich hänge da jetzt echt lange dran, aber erst mal eine Grafik.

    http://imageshack.us/photo/my-images/607/classesg.png/

    Ich habe 2 Objekttypen A und B. Die Abhängigkeit zwischen ihnen lässt sich mit folgender Funktion beschreiben:

    Result myFunc(A& a, B& b,Arg& some_arg){
         Result temp=a.foo(some_arg);
         temp=b.transform(temp);
         return a.bar(temp);
    }
    

    Hierbei wird in a.foo ein interner Zustand geschrieben, der nur von some_arg abhängt, insbesondere nicht vom vorherigen Zustand. Das Ergebnis von foo wird dann irgendwie transformiert und das Resultat an A.bar übergeben wo der zuvor geschriebene Zustand sowie die transformierte Eingabe verwendet werden, das Endergebnis zu berechnen.

    Soweit, so Abstrakt. Nun hat der Ansatz zwei Probleme:
    1. auch wenn ich es gerne würde, dieses Design macht es unmöglich, myFunc zu parallelisieren und es gibt Objekte vom Typ A die gar nicht parallelisierbar sind. Die Berechnungen sind aber so aufwendig, dass ich mehrere CPUs verwenden muss, sonst werde ich nie fertig (wir reden hier von Monaten Rechenzeit).

    2. Das Interface ist wackelig. in A.bar gibt es keinen Hinweis darauf, dass A.foo vorher aufgerufen werden muss. Wir haben das zwar dokumentiert, aber schon gemerkt, dass die Studenten, die mit dem Interface arbeiteten dies nicht so wirklich realisiert haben und sich über falsche Ergebnisse wunderten. Es gilt also, das Interface Studentensicher zu machen.

    Für mögliche Problemlösungen gibt es zwei weitere Probleme:
    1. es kann sein, dass alle Funktionen von A unglaublich schnell berechenbar sind und nur die schiere Masse der Funktionsaufrufe von myFunc das Problem schwer machen. Mögliche Lösungen müssen daher möglichst overheadfrei sein.
    2. A ist nicht kopierbar (A ist Basisklasse und das Interface erlaubt leider kein clone :()

    Nun zu meinem Lösungsansatz:
    Ich habe versucht, den Zustand einfach aus der Klasse zu ziehen und als externes Objekt zu modellieren. Als Zustandstyp habe ich den schlechtmöglichsten Typ gewählt: std::vector<double>. Das Ding habe ich etwas weiter aufgebohrt zu einem "State"-Typ der subrange-operationen anbietet. das war notwendig für den Fall, dass A aus mehreren Objekten besteht, die jeweils ihren eigenen State haben.

    Das Interface sieht nun so aus

    struct A{
        virtual std::size_t stateSize(some_arg);
        virtual Result foo(some_arg, State& state)=0;
        virtual Result bar(transformedFoo, State const& state)=0;
    };
    

    und myfunc habe ich geändert zu:

    Result myFunc(A& a, B& b,Arg& some_arg){
         State state(a.stateSize(some_arg));
         Result temp=a.foo(some_arg,state);
         temp=b.transform(temp,state);
         return a.bar(temp);
    }
    

    pro:
    1. einfach
    2. meistens schnell
    3. funktioniert in 99% aller Fälle
    contra:
    1. unwartbarer code
    2. fehleranfällige Indexschieberei (in state befinden sich vektoren, matrizen, scalare... all das muss ich später aus dem Ding wieder raus popeln)
    3. funktioniert nicht immer

    In der Tat ist es so schlimm, dass ich jetzt bei der Portierung eines Objektes einfach kapituliert habe, weil ich die Fehler nicht unter Kontrolle kriegen konnte.

    Wenn ich in einer perfekten generischen Welt leben würde, würde ich das machen wollen:

    struct A{
        struct State{...};//jeder Typ hat eigenen state
        Result foo(some_arg, State& state)=0;
        Result bar(transformedFoo, State& state)=0;
    };
    

    Aber ich lebe leider in einer unperfekten objektorientierten Welt, wo ich nur abstrakte Basisklassen haben kann.

    Hat irgendjemand eine schnelle Lösung für mein Problem? Vielleicht auch einen anderen Ansatz? Ich bin für alles offen.

    Danke fürs durchlesen 🙂 (ich bin auch nach Schreiben dieses Romans ratlos)



  • Not every problem is an object, dude.

    Fuer Prallelisierung eignen sich in erster Linie zustandslose/freie Funktionen. Es scheint, dass du wenig Erfahrung damit hast.

    schnelle Lösung ... für alles offen

    Das widerspricht sich.

    Wenn ich in einer perfekten generischen Welt leben würde

    Machbar, beispielsweise durch Templates, type erasure oder pimpl.



  • Edit: Moment, da fehlt ein virtual. 😉
    Edit2: Okay

    Was spricht dagegen?

    class A
    {
    public:
      virtual Result baz(B& b, Arg& arg)
      {
        Zustand zustand;
        return bar(zustand, b.transform(foo(zustand, arg)));
      }
    
    private:
      virtual Result foo(Zustand& zustand, Arg& arg); // static?
      virtual Result bar(const Zustand& zustand, Result r); // static?
    };
    


  • knivil schrieb:

    Not every problem is an object, dude.

    Fuer Prallelisierung eignen sich in erster Linie zustandslose/freie Funktionen.

    Was ja meinem Problem entspricht: wie kriege ich das Ding Zustandsfrei?. Das es diesen Zustand gibt, ist ein blödes Implementationsdetail, weil wir gerne effizienten Code haben wollen und deswegen nicht Ergebnisse großer Matrix-Matrix Multiplikationen weg werfen wollen. (oder im Falle von günstigen Funktionen ein Skalarprodukt oder das Ergebnis eines exp-Aufrufs).

    schnelle Lösung ... für alles offen

    Das widerspricht sich.

    Nur, wenn man es drauf anlegt, dass es sich widerspricht. Ich bin erst mal für jede mögliche Lösung offen, würde dann aber Benchmarken, wie teuer sie ist.

    @cookie Es gibt verschiedene Versionen von myFunc, die sich da in Kleinigkeiten unterscheiden. Die Aufteilung der Funktionen in dieser Art und Weise war ein iterativer Prozess über jetzt mehrere Jahre, und das ist die einzige Version, die alles kann, was wir brauchen.



  • Was spricht dagegen?

    Warum ueberhaupt eine Klasse?



  • das Ding hat Parameter, eine konfigurierbare Struktur und ist alles in allem recht kompex.

    Es geht darum:
    AbstractModel
    Das ist noch eine "alte" doku. Der Code an dem ich arbeite hat noch keine öffentlich zugängliche Dokumentation. Das ist also die Version ohne externen State und ich muss eval() und die (ersten) Ableitungen unter einen Hut kriegen.

    Es wird alles komplizierter dadurch, dass ich durch die Form der Ableitungen in der Lage bin, die Modelle zu verketten und dann zum Beispiel Parameterableitungen für das gesamte Modell bilden kann.



  • otze schrieb:

    @cookie Es gibt verschiedene Versionen von myFunc, die sich da in Kleinigkeiten unterscheiden. Die Aufteilung der Funktionen in dieser Art und Weise war ein iterativer Prozess über jetzt mehrere Jahre, und das ist die einzige Version, die alles kann, was wir brauchen.

    Gibt es nun mehrere Versionen, oder nur eine? Und ist das so schwer zu ändern? Sieht für mich nach copy pasta aus. Wenn das nicht geht, sollte man das eventuell näher beschreiben, sonst macht es ja wenig Sinn Vorschläge zu machen, bei denen man garnicht weiß was geändert werden kann und was nicht.



  • cooky451 schrieb:

    Gibt es nun mehrere Versionen, oder nur eine?
    Und ist das so schwer zu ändern? Sieht für mich nach copy pasta aus. Wenn das nicht geht, sollte man das eventuell näher beschreiben, sonst macht es ja wenig Sinn Vorschläge zu machen, bei denen man garnicht weiß was geändert werden kann und was nicht.

    wir haben bislang 4 verschiedene Versionen von myfunc, sowie 10 Modelle. Tendenz steigend. Wir müssen auch für die Zukunft etwas Luft nach oben haben, da sich die Bibliothek in einem Forschungsbereich befindet und rapid-prototyping möglich sein soll(also mal eben fix eine Idee ausprobieren und schauen, ob das funktioniert). Das Interface muss also flexibel sein und ich glaube, dass wir bereits jetzt die Flexibilität voll ausnutzen.

    Deswegen glaube ich nicht, dass wesentliche Aspekte des Interfaces geändert werden können. Weitere Parameter oder neue Funktionen hinzuzufügen ist okay, da sich die Änderungen nicht weit fortpflanzen. Aber eine Änderung des grundlegenden Klassendesigns wird wohl eher nicht drin sein :(.

    Oh und ich sitze alleine an der Implementation dieser Änderung ;).

    //edit Es ist auch ein semantisches Problem. was myFunc im echten problem macht, ist eine Fehlerfunktion darstellen, das heißt messen wie gut das Modell A die gegebene Zielsetzung (zum Beispiel dargestellt durch 😎 erfüllt und einen Gradienten zu berechnen, der angibt, wie das Modell geändert werden muss, um das gegebene Ziel zu erreichen. Das Modell selbst hat keine Ahnung von Zielen, es transformiert eine Eingabe nur in eine Ausgabe. Und die Ziele selbst sind zwischen den verschiedenen Lernparadigmen völlig unterschiedlich(wenn ich Regression machen will, kann ich das völlig anders aufziehen, als wenn ich clustern muss).

    Wenn wir auf eine schnelle Implementation verzichten wollten, würden wir den internen Zustand gar nicht brauchten (das ist ein reines Implementationsdetail für 99% aller Modelle und die restlichen 1% kann man eh nicht parallel berechnen). Und dann wäre das Design so schon ziemlich gut. Das Problem entsteht erst dadurch, dass wir den internen Zustand einführen, weil wir gerne auf single core Faktor 2 schneller wären. Und weil wir das nun auch noch parallelisieren wollen, muss der Zustand wieder aus dem Objekt raus.

    //edit 2 angenommen, ich würde erstmal nur vom std::vector<double> los kommen wollen um das Interface zwar nicht unbedingt besser zu machen, aber einfacher zu verwenden. würde ich dann besser fahren, wenn ich einfach eine Basisklasse für States verwende und dann mit einem boost::polymorphic_cast oder ähnlichem in den verwendenden Klassen einen downcast mache?

    oder vielleicht etwas aufgebohrt als sowas:

    //skizze
    class State{
    private:
        polymorphic_state_wrapper* wrapper;
    public:
        State();
        template<class ModelState,class Args>
        void createState(Args args){
             wrapper = new MyStateWrapper<ModelState>(args);
        }
        bool hasState(){
            return wrapper != 0;
        }
        template<class ModelState>
        ModelState& getState(){
            return boost::polymorphic_downcast<MyStateWrapper<ModelState>* >(wrapper)->state;
        }
    };
    


  • Könntest du nicht eine Art "Super-State" einführen, der Platz für alles hat? Damit hättest du den Kram nur noch einmal. Ist zwar Speicherplatzverschwendung, aber wenn das akzeptabel ist, sollte es eigentlich ziemlich simpel sein für den Rest des Codes.



  • wie meinst du das genau mit dem Super-State? Meinst du so eine Art globalen Zustand bei dem sich jedes Objekt registriert und dann rein schreibt?

    also sowas wie: "ich bin objekt x in thread y gib mir mal bitte meinen State"?

    also in code ausgedrückt:

    struct MyModel{
    private:
        class State{...};
        unsigned int m_objectID;
    public:
        MyModel(){
            m_objectID=GlobalState::registerObject();//schreit nach RAII
        }
        ~MyModel(){
            GlobalState::unregister(m_objectID);
        }
        virtual Result foo(some_arg){
            State& state = GlobalState::getState<State>(m_objectID);//automatischer state für jeden thread
            state = ...;
            //was auch immer sonst berechnet werden muss
        }
        virtual Result bar(transformedFoo)const{
            State const& state = GlobalState::getState<State>(m_objectID);
            //nutze state
        }
    };
    

    sieht eigentlich auf den ersten Blick elegant aus. Eventuell macht man da am besten noch eine ordentliche Portion RAII drum und dann erhält man:

    struct MyModel{
    private:
        class State{...};
        GlobalState<State> m_state;
    public:
        virtual Result foo(some_arg){
            State& state = m_state.state();
            state = ...;
            //was auch immer sonst berechnet werden muss
        }
        virtual Result bar(transformedFoo)const{
            State const& state = m_state.state();
            //nutze state
        }
    };
    

    voll cool eigentlich!
    //edit man muss nur irgendwie garantieren, dass die thread-ids identisch bleiben :/. sollte kein Problem sein, solange das Threading von außen kommt. wenn man aber erst in foo() oder bar() threaden würde, wäre das eventuell finster...

    //edit X bwaah jetzt ist aber nicht mehr kodiert, dass bar nur nach foo aufgerufen werden kann. arrghl



  • Das meinte ich eigentlich nicht.^^ Das Problem war doch, dass der Typ von "State" abhängig ist von A. Du hast dann den Zustand rausgezogen und vector<double> als "generische" Schnittstelle genommen. Meine Idee war jetzt, einfach einen State zu machen, der für alle A funktioniert. Dann hat man vielleicht ein paar Stellen drin, die nur manchmal gebraucht werden, aber man muss da nichts mehr

    später aus dem Ding wieder raus popeln.

    😉



  • otze schrieb:

    jetzt ist aber nicht mehr kodiert, dass bar nur nach foo aufgerufen werden kann. arrghl

    Mach doch so etwas:

    Result myFunc(A& a, B& b,Arg& some_arg){
         FooTransformed<Result> fromFoo = a.foo(some_arg);
         FooTransformed<Result> temp    = b.transform(temp);
         Result res = a.bar(temp);
         return res;
    }
    

    FooTransformed<Result> ist eine Klasse die nur aus Result besteht. Die einzige Eigenschaft, die sie hat, ist, dass sie nur von A erzeugt werden kann:

    struct FooTransformed<Result> {
       Result* operator->() { return &r; }
       friend class A;
    private:
       Result r;
       FooTransformed(Result r) : r(r) {}
    };
    struct A{
        virtual std::size_t stateSize(some_arg);
        FooTransformed<Result> foo(some_arg, State& state)
        { return FooTransformed(foo_impl(arg, state)); }
        virtual Result bar(FooTransformed<Result>, State const& state)=0;
    protected:
        virtual Result foo_impl(some_arg, State& state)=0;
    };
    


  • cooky451 schrieb:

    Das meinte ich eigentlich nicht.^^ Das Problem war doch, dass der Typ von "State" abhängig ist von A. Du hast dann den Zustand rausgezogen und vector<double> als "generische" Schnittstelle genommen. Meine Idee war jetzt, einfach einen State zu machen, der für alle A funktioniert. Dann hat man vielleicht ein paar Stellen drin, die nur manchmal gebraucht werden, aber man muss da nichts mehr

    später aus dem Ding wieder raus popeln.

    😉

    Das ist bei der Variabilität schon schwierig (wir haben im worst case eine dynamische Anzahl von matrizen, vektoren, skalaren und anderem Kleinkram...). Wäre sicherlich besser als die jetzige Version...andererseits ist alles besser als die jetzige Version. Und der Ansatz führt noch immer nicht zu Code, bei dem die Zustandsvariablen ordentliche Namen haben.

    hmm...

    @footrans ja, an sowas habe ich auch schon gedacht. Es gibt an der Stelle aber eine einfachere Lösung, die man eventuell verwenden könnte:

    Result myFunc(A& a, B& b,Arg& some_arg){
         Result fooResult = a.foo(some_arg);
         Result temp    = b.transform(temp);
         Result res = a.bar(fooResult,temp);
         return res;
    }
    

    da bar die Ableitung der Funktion foo ist und wir häufig den Fall haben, dass wir die Ableitung in der form f'(x)=g(f(x)) ausdrücken können, macht es sinn, an der Stelle den Funktionswert zu erwarten.

    Muss ich aber mit meinen Leuten abklären, was die davon halten.

    OT:

    Ich bin noch immer unentschlossen. Der globale State, wie ich ihn vorgeschlagen habe ist zwar nett, insbesondere weil er die Implementationsdetails nicht nach außen trägt, andererseits sehe ich schon jetzt, dass das nur eine leaky-abstraction ist und ich in manchen Fällen trotz des versteckten Zustandes dafür Sorge tragen muss, dass ich nicht aus versehen wichtige Dinge überschreibe.

    Genau so ginge ja auch:

    struct MyModel{
    private:
        class State{...};
    public:
        virtual Result foo(some_arg, AbstractState& state)const{
            State& state = state.get<State>();
            state = ...;
            //was auch immer sonst berechnet werden muss
        }
        virtual Result bar(transformedFoo, AbstractState const& state)const{
            State& state = state.get<State>();
            //nutze state
        }
    };
    

    Arrgh Entscheidungen...


Anmelden zum Antworten