Performanter Zugriff auf "Datenklasse". 2 Varianten..



  • Moin!
    Ich bins mal wieder mit einem kleinen, relativ theoretischen Problem..

    Ich hätte hier 2 Möglichkeiten und mich würde interessieren, welche performance-mäßig die bessere ist. Ich hoffe mal, dass man sich ausnahmsweise nicht entweder für Speicher oder CPU entscheiden muss.. Ich habe aber ehrlich gesagt keine Ahnung, wie/was der Compiler so optimiert.

    Mal angenommen ich habe zum einen eine Datenklasse, die also haufenweise Member unterschiedlichen Datentyps enthält, ein paar ints, doubles, pointers und zB structs.
    Außerdem habe ich noch eine Klasse, mit der ich auf die Datenklasse via getter-Funktionen zugreifen kann.
    Ist es nun besser,
    1. einen Datenklassen-Pointer ("D *data") zu speichern und dann in jedem getter mit "data->..." zu arbeiten, oder
    2. pro Datenklassen-Member einen Pointer (z.B. int *iP) anzulegen, mit dem ich dann im jew. getter etwas direkter arbeiten kann??

    Bei 1. wäre es (unoptimiert) eine Operation mehr ("." + "*"), bei 2. dafür wesentlich mehr Speicherbedarf (je nach Datenklassen-Größe).
    Ich bin mir relativ sicher, dass bei 1. die 2fache Operation wegoptimiert werden kann. (Stimmt doch, oder?)
    Und was ich mich noch frage: Kann der Compiler eigentlich auch Speicherplatz wegoptimieren?
    Ist also evtl 1. und 2. nach dem optimieren 100%ig identisch, bzw. ist 1. in jedem Fall besser als 2., oder umgekehrt?!

    PS. Noch eine kleine Gewissensfrage: Welchen Stil bevorzugt ihr:
    A) "int ip", oder
    😎 "int
    ip", ... oder sogar
    C) "int * ip" ?

    Ich habe mir ursprünglich A angewöhnt, eigentlich nur um sowas wie "int x, *y" schreiben zu können, was ich aber nie mache, wie sich gezeigt hat..
    Gcc ist eher der B-Typ.. Das finde ich irgendwie verständlich. Hätte ich auch so gemacht, wenn ich 'damals' gewusst hätte, dass mein einziges Entscheidungskriterium bei mir nie zur Anwendung kommt. Also wie seht ihr das?!

    MfG
    0xMöbius



  • Wenn ich mir Gedanken um Performance machen würde, wären getter so ziemlich das erste was raus fliegt.

    Ansonsten: messen!



  • Also in 99,5% aller Applikationen dürfte der vermeintliche Performance Unterschied absolut keine Rolle spielen.

    Mir persönlich gefallen Pointer auf native Datentypen überhaupt nicht, da sie unnötigerweise semantische Komplexität in den Source Code einführen.

    Würde mich jedenfalls sehr wundern, wenn Int*, int& oder sogar int Byte value Performance technisch irgend einen Unterschied machen würde.



  • Wenn du schon nach Performance fragst, dann bette deine Datenklasse direkt in deine andere Klasse ein:

    class A
    {
      Data data;
    }
    

    So benötigst du dann überhaupt keine Zeiger (Indirektion) mehr beim Aufruf.



  • Noch besser wäre 3) keine "Datenklasse" zu haben, sondern ein sauberes Design... 😉



  • manni66 schrieb:

    Wenn ich mir Gedanken um Performance machen würde, wären getter so ziemlich das erste was raus fliegt.

    Ansonsten: messen!

    Ja, auf jeden Fall messen. Allerdings wären bei mir wahrscheinlich die Getter das letzte das rausfliegt. Ich glaube es gibt kaum eine Klasse von Funktionen,
    die sich besser vom Compiler inlinen lassen, als die ganzen trivialen Getter, die man schreibt, wenn man das Getter/Setter-Konzept erbarmungslos durchzieht.

    Zur Eingangsfrage: Pointer auf Data und Pointer auf Member wird wie schon erwähnt wahrscheinlich keinen messbaren Unterschied machen.
    Offsets zu Addressen addieren ist quasi der zweite Vorname so ziemlich jeder CPU - eine solche Operation kann man de facto als No-op abhaken,
    es sei den man misst tatsächlich etwas anders.

    Interessanter für die Performance dürfte sein wie das Zugriffsmuster auf die Data-Klassen aussieht, und wo diese im Speicher liegen.
    Sind die Data-Intstanzen willkürlich im Speicher verteilt, würde ich aus dem Bauch heraus auch bevorzugen, Data zu einem eingebetteten Member zu machen,
    da die Member von Data dann im Speicher direkt neben den Objekt-Membern liegen.
    CPUs sind heutzutage nämlich die meiste Zeit damit beschäftigt, auf den Arbeitsspeicher zu warten. Daher ist wenn möglich linearer Zugriff auf den Speicher zu bevorzugen,
    da man davon ausgehen kann dass die "nachfolgenden" Daten bereits in den CPU-Cache geladen wurden. Dereferenzierungen wirken sich also oft ungünstig auf die Performance aus...

    dot schrieb:

    Noch besser wäre 3) keine "Datenklasse" zu haben, sondern ein sauberes Design... 😉

    ... allerdings nicht immer, daher wäre ich selbst etwas vorsichtiger, von "unsauberem Design" zu sprechen, ohne alle Details zu kennen 😃
    Ein Beispiel, wo eine Datenklasse nicht unbedingt eine schlechte Idee sein muss:

    struct Data
    {
        Vec3 position;
        Vec3 velocity;
    };
    
    std::vector<Data> positionAndVelocity;
    
    struct Object
    {
        const Vec3& getPosition() { return data->position; }
        const Vec3& getVelocity() { return data->velocity; }
    
        std::string name;
        Data*       data;
        OtherData*  otherData;
    }
    
    void updateObjectPositions()
    {
        for (auto& data : positionAndVelocity)
            data.position += data.velocity;
    }
    

    Angenommen man hat es mit sehr vielen Objekten zu tun, und updateObjectPositions ist eine performance-kristische Funktion, die alle Zugriffe auf die Objektdaten über die Object -Instanzen dominiert.
    Hier könnte tatsächlich ein Data* einen Performancevorteil bringen, da die für die kristische Schleife relevanten Daten kompakt im Speicher liegen und linear darauf zugegriffen wird.

    0xMöbius schrieb:

    PS. Noch eine kleine Gewissensfrage: Welchen Stil bevorzugt ihr:
    A) "int ip", oder
    😎 "int
    ip", ... oder sogar
    C) "int * ip" ?

    Hehe... das ist so eine kontroverse Glaubensfrage. Ich persönlich bevorzuge int* ip da der Typ der Variable ein "int-Pointer" ist.
    int *ip sieht man auch häufig, und so habe ich es früher auch geschrieben, vor allem weil sich diese Form besser mit der Syntax verträgt, mit der man mehrere Variablen deklariert: int *ip1, *ip2, *ip3; .
    Ich halte aber mittlerweile das "int-Pointer"-Argument für stärker, da intuitiver. Ansonsten: Mach es wie es dir am besten zusagt, Hauptsache überall konsistent ;).

    Finnegan



  • Wenn es wirklich um die Wurst geht: messen.
    Das dumme dabei ist nur, dass es wenig Sinn macht mit einem Testprogramm zu messen das dann nicht genau die Verhältnisse simuliert (welche Threads laufen gleichzeitig, Cache-Auslastung, welcher Code läuft vorher/nachher etc.) wie man sie im fertigen Programm dann hätte. Was oft recht aufwendig ist.

    Daher, und speziell auch wenn es nicht um die Wurst geht: die einfachere Lösung nehmen, also nur einen Zeiger.
    x86/amd64 kann solche Zugriffe in einem einzigen Befehl machen. Ob es dann intern zu mehreren Microcode-Operationen zerlegt wird weiss ich nicht. Wird auch vom genauen CPU Modell abhängen. Der Unterschied wird aber auf jeden Fall nicht sehr gross sein. Additionen sind sehr billig, und wenn sie wie in diesem Fall nichtmal ein Register kosten...



  • Danke schon mal für die vielen Antworten.
    Also nach allem, was ich so über c++ aufgeschnappt habe bin ich eigentlich ganz bei Finnegan.
    Was das Messen angeht: Unabhängig von dieser Situation meine ich zumindest gemessen zu haben, dass Pointer-Dereferenzierungen wegoptimiert wurden. Zumindest hat es sich auf die Geschwindigkeit ausgewirkt, ob man mit oder ohne "-o.."-Flag kompiliert hat. Wie würde ich denn idealerweise die größe im Speicher messen können? Mit debugging habe ich mich nicht so ausgiebig beschäftigt - ich mache halt keine Fehler ;).. ..ernsthaft, ich habe bislang eher mit der Konsole nach bugs gesucht.

    @manni66: Ich denke auch, dass sie ge-inlined werden, zumindest wenn man sie direkt in der Klasse definiert.

    @Pointer auf int: Ist schon richtig, dass es oft nicht viel Sinn ergibt, wenn die Größe des Datentyps maximal der des Pointers auf ihn entspricht, mit dem Pointer zu arbeiten. Lässt sich hier aber nicht vermeiden.

    @Th69: Damit würde ich aber die Trennung von Daten und Zugriff aufheben, oder sehe ich das falsch?

    @dot: In diesem Fall wäre eine Datenklasse aber ein sauberes Design (denke ich). Du kaufst dir ja auch nicht zu jeder Tasse Kaffee einen neuen Arm dazu, sondern benutzt den, den du schon hast.

    @Finnegan

    Offsets zu Addressen addieren ist quasi der zweite Vorname so ziemlich jeder CPU - eine solche Operation kann man de facto als No-op abhaken,
    es sei den man misst tatsächlich etwas anders.

    Ich meine als Messergebnis heraus bekommen zu haben, dass es vor allem der Job des Compilers ist, eben weil "", "-o1", "-o2",.. einen Unterschied gemacht hat.

    Interessanter für die Performance dürfte sein wie das Zugriffsmuster auf die Data-Klassen aussieht, und wo diese im Speicher liegen.
    Sind die Data-Intstanzen willkürlich im Speicher verteilt, würde ich aus dem Bauch heraus auch bevorzugen, Data zu einem eingebetteten Member zu machen,
    da die Member von Data dann im Speicher direkt neben den Objekt-Membern liegen.
    CPUs sind heutzutage nämlich die meiste Zeit damit beschäftigt, auf den Arbeitsspeicher zu warten. Daher ist wenn möglich linearer Zugriff auf den Speicher zu bevorzugen,
    da man davon ausgehen kann dass die "nachfolgenden" Daten bereits in den CPU-Cache geladen wurden. Dereferenzierungen wirken sich also oft ungünstig auf die Performance aus...

    Das ist mal nen Punkt, an den ich noch garnicht gedacht habe.. Das macht es komplizierter..
    Wenn ich jetzt z.B. Daten habe, auf die auf unterschiedliche Arten zugreifen möchte (also Klassen: Daten, Zugriff1, Zugriff2, ...), dann ist das Einbetten doch nicht machbar, oder?

    Angenommen man hat es mit sehr vielen Objekten zu tun, und updateObjectPositions ist eine performance-kristische Funktion, die alle Zugriffe auf die Objektdaten über die Object-Instanzen dominiert.
    Hier könnte tatsächlich ein Data* einen Performancevorteil bringen, da die für die kristische Schleife relevanten Daten kompakt im Speicher liegen und linear darauf zugegriffen wird.

    An sowas hatte ich ursprünglich gedacht. War nur die Frage, ob ich in "Object"
    ein "Data* data;" oder viele "Vec3* position; Vec3* velocity; ..." speichere.

    Hehe... das ist so eine kontroverse Glaubensfrage.

    Tjaa.. Offenbar viele Atheisten hier :D..
    D.h. solche Einzeiler wie "int *ip1, *ip2, *ip3;" schreibst du garnicht? Ganz drauf verzichten möchte ich nicht, nur Mischen ("int i, *ip;") gefällt mir nicht.
    Ist mir gerade eingefallen: Für Fall A spricht aber die in Stein gemeißelte Syntax "int i[]" (statt int[] i) ...ob der entscheidende Teil nun vor oder hinter dem Namen steht sei mal egal - hauptsache am i..
    Dagegen spricht, dass der Compiler von "int[]" spricht. Also so wirklich konsistent ist das nicht...

    @hustbaer:

    Das dumme dabei ist nur, dass es wenig Sinn macht mit einem Testprogramm zu messen das dann nicht genau die Verhältnisse simuliert (welche Threads laufen gleichzeitig, Cache-Auslastung, welcher Code läuft vorher/nachher etc.) wie man sie im fertigen Programm dann hätte. Was oft recht aufwendig ist.

    Ist, was Threadding angeht, vielleicht nicht der Weißheit letzter Schluss, aber zumindest als Anhaltspunkt wäre es doch ausreichend, das Programm in Teilen zu vermessen, also die Klasse separat.

    Daher, und speziell auch wenn es nicht um die Wurst geht: die einfachere Lösung nehmen, also nur einen Zeiger.
    x86/amd64 kann solche Zugriffe in einem einzigen Befehl machen.

    Du sprichtst jetzt auch vom Offset+Dereferenzieren (->)? Da frage ich mich natürlich, wo das noch so gilt?! Was ist z.B. mit Dereferenzieren+Offset, oder mehrfach Deref?

    Und macht es optimierungstechnich einen Unterschied, ob ich mit Zeigern oder Referenzen/ const-Ptr arbeite? So wie ich mir das vorstelle, sollten letztere doch besser wegoptimierbar sein, also letztlich nicht dereferenziert werden müssen, sonder gleich mit dem jew. Wert gearbeitet werden können, da der Compiler sicher sein kann, dass sich die angegebene Adresse nicht ändern kann!?

    Da es mir, so wie ich das sehe, schwer fallen wird, den tatsächlichen Speicherplatzbedarf zu ermitteln. Wie ist denn eure Meinung dazu, ob Speicher überhaupt wegoptimiert werden kann? Ich hatte ursprünglich gehofft, dass es einfach keinen Sinn machen würde..

    PS.: Sagt mal, kompiliert euer Compiler, oer compiliert euer Compiler, oder kompiliert euer Kompiler??



  • 0xMöbius schrieb:

    Das dumme dabei ist nur, dass es wenig Sinn macht mit einem Testprogramm zu messen das dann nicht genau die Verhältnisse simuliert (welche Threads laufen gleichzeitig, Cache-Auslastung, welcher Code läuft vorher/nachher etc.) wie man sie im fertigen Programm dann hätte. Was oft recht aufwendig ist.

    Ist, was Threadding angeht, vielleicht nicht der Weißheit letzter Schluss, aber zumindest als Anhaltspunkt wäre es doch ausreichend, das Programm in Teilen zu vermessen, also die Klasse separat.

    Kann halt passieren dass bei so einer Messung eine Variante am besten abschneidet die, wenn man sie in einem grösseren Programm verwendet, halt doch nicht ideal ist.

    0xMöbius schrieb:

    Daher, und speziell auch wenn es nicht um die Wurst geht: die einfachere Lösung nehmen, also nur einen Zeiger.
    x86/amd64 kann solche Zugriffe in einem einzigen Befehl machen.

    Du sprichtst jetzt auch vom Offset+Dereferenzieren (->)? Da frage ich mich natürlich, wo das noch so gilt?! Was ist z.B. mit Dereferenzieren+Offset, oder mehrfach Deref?

    x86/amd64 kann als Adressieungsart Register1 + Register2 * (1, 2, 4 oder 8) + Konstante

    Also mit...
    p : Zeiger auf eine Struktur
    a : Ein Array welches Member dieser Struktur ist, und dessen Elemente 1, 2, 4 oder 8 Byte gross sind
    n : Ein Integer (int, short, ...)
    x : Ein Member der Elemente von a

    Könnte die Adresse von p->a[n].x direkt in einem Befehl berechnet werden. p und n kämen in die beiden Register, die Grösse von a ist der Multiplikator für das n Register und der Offset (die Konstante) berechnet sich aus dem Offset von a plus dem Offset von x innerhalb der Elemente von a .

    Diese Adressierungsart kann so ziemlich überall wo man Speicher adressiert verwendet werden, also auch bei einem load/store. Heisst: auch das "Dereferenzieren" geht noch in einem Befehl.

    Und mehrfach dereferenzieren kann keine mir bekannte CPU in einem Befehl. Würde auch kaum Sinn machen, da das Laden von Werten aus dem Speicher mächtig lange dauert.

    0xMöbius schrieb:

    Und macht es optimierungstechnich einen Unterschied, ob ich mit Zeigern oder Referenzen/ const-Ptr arbeite?

    Nö, ist Wurst.

    0xMöbius schrieb:

    So wie ich mir das vorstelle, sollten letztere doch besser wegoptimierbar sein, also letztlich nicht dereferenziert werden müssen, sonder gleich mit dem jew. Wert gearbeitet werden können, da der Compiler sicher sein kann, dass sich die angegebene Adresse nicht ändern kann!?

    const heisst "du sollst nicht ändern". Es heisst weder "du kannst nicht ändern" noch "keiner kann ändern". const hilft dem Compiler bei solchen Sachen also leider gar nicht.
    Was ihm dagegen massiv hilft ist inlining.

    0xMöbius schrieb:

    Da es mir, so wie ich das sehe, schwer fallen wird, den tatsächlichen Speicherplatzbedarf zu ermitteln. Wie ist denn eure Meinung dazu, ob Speicher überhaupt wegoptimiert werden kann? Ich hatte ursprünglich gehofft, dass es einfach keinen Sinn machen würde..

    Hab ja schon geschrieben: nimm die Variante mit einem Zeiger. Ich würde da an deiner Stelle keinen weiteren Gedanken dran verschwenden.

    0xMöbius schrieb:

    PS.: Sagt mal, kompiliert euer Compiler, oer compiliert euer Compiler, oder kompiliert euer Kompiler??

    Mein Compiler kompiliert.





  • 0xMöbius schrieb:

    Ich habe aber ehrlich gesagt keine Ahnung

    0xMöbius schrieb:

    Also nach allem, was ich so über c++ aufgeschnappt habe...

    Du bist noch weit davon weg, an Performance zu denken. Ich wette dein Klassendesign und deine Algorithmen haben 1000 mal Optimierungsmöglichkeiten um die Performance zu verbessern, als so ein Pointerkram.



  • Danke hustbaer! Das sind sehr nützliche Infos. Kommt gleich auf meinen Notizzettel am Bildschirm.. ..und erklärt auch, warum bei diversen Containern keine Einbußen zu erwarten sind.

    @Zusammengefasst:

    Zusammengefasst schrieb:

    dein Klassendesign und deine Algorithmen haben 1000 mal (...) Performance

    Danke, sehr freundlich. Das liegt auch an diversem Pointerkram.

    ..Das Beispiel hat nichtmal 1000 Ausdrücke, geschweige denn 1000 Zeichen. Die Wette gehe ich gerne ein.


Log in to reply