Bester Stil für diverse Funktionen im Vector2D



  • Ich habe eine Klasse Vector2D und wollte wissen welcher Stil für welche Funktionen besser ist?

    Normierung des Vektors:
    Innerhalb der klasse (ändert den vektor)

    Vector2D& norm() {
    		float len = length();
    		if (len > 0) { x /= len; y /= len; } 
    		else { /*cerr << "Division by Zero in Vector2D.norm()!" << endl; */}
    		return *this;
    	}
    

    oder außerhalb (legt eine Kopie an, benötigt oben genannte norm() bzw. wenn nicht vorhanden müsste manuell gemacht werden)

    Vector2D norm(const Vector2D& v) {
    	Vector2D retVal(v);
    	retVal.norm();
    	return retVal;
    }
    

    oder beide?

    Dann Abstand zu einem anderen Vektor:
    Besser als

    vec.distance(otherVec)
    

    oder

    distance(vec, otherVec)
    

    Generelle Empfehlungen bei solchen Funktion?



  • Für Klassen, die man oft temporär erstellt und dann wieder wegwirft, wie gerade Vector2D, biete ich i.d.R. beide Varianten an, verdeutliche dies aber mit einem unterschiedlichen Bezeichner:

    Vector2D& Vector2D::normalize() {
    	const auto len = length();
    	assert( len > 0 );
    	x /= len;
    	y /= len;
    	return *this;
    }	
    
    Vector2D normalized(Vector2D v) {
    	return v.normalize();
    }
    

    Selbiges für andere kleine mathematische Objekte wie komplexe Zahlen, Quaternionen, Brüche etc.
    Für große Klassen biete ich nur die erste Variante an, da man so die teuren Kopierkosten nicht übersehen kann.

    PS: nimm doubles statt floats, die sind m.E.n. auf AMD64 schneller.
    EDIT: Siehe unten.



  • Ich tendiere dazu so etwas ausschliesslich in Form simpelst gehaltener freier Funktionen umzusetzen, also wie deine zweite Variante nur ohne explizite temporäre lokale Variablen oder solche Feinheiten wie Referenzen:

    float dot(Vector2D x0, Vector2D x1)
    {
        return x0.x * x1.x + x0.y * x1.y;
    }
    
    float length(Vector2D x)
    {
        return sqrt(dot(x, x));
    }
    
    Vector2D normalize(Vector2D x)
    {
        return x / length(x);
    }
    
    Vector2D dist(Vector2D x0, Vector2D x1)
    {
        return length(x1 - x0);
    }
    

    Die freien Funktionen haben den Vorteil, dass sich Formeln und Berechnungen, welche diese Funktionen nutzen, auf (mathematisch) natürlichere Weise und meist in nur eine Zeile schreiben lassen, ohne irgendwelche Verrenkungen mit temporären Variablen. Das macht den Code, der die Vektor-Klasse letztendlich verwendet kürzer und verständlicher. Auch überfrachte ich eine Vektor-Klasse nur ungerne mit Funktionen, die nur Brechnungen mit dem Datentyp darstellen und keine Grundfunktionen des Typs sind, wie Konstruktion oder Zugriff auf ein Element (einzige Ausnahme: Zusammengesetzte Operatoren wie += ).

    Ebenso darf man davon ausgehen, dass moderne Compiler gerade bei einer so simplen Datenstruktur wie einem 2D-Vektor solchen Code hervorragend optimieren werden: Die meisten Audrücke mit solchen Funktionen werden beim kompilieren sehr wahrscheinlich trotz einer gewissen Schachtelung aussliesslich zu inline-Instruktionen zerfallen, die direkt auf den CPU-Registern arbeiten. Die Referenzen und temporären Variablen in deinen Funktionen sind also mit ziemlicher Sicherheit vergebliche Liebensmüh, die den Compiler eher dabei behindern, guten Code zu erzeugen bevor sie tatsächlich einen Vorteil bringen.

    Ich maße mir nicht an über den "besten Stil" zu urteilen, aber mit dem oben angewandten KISS-Prinzip macht man erstmal nichts falsch.


  • Mod

    Finnegan schrieb:

    Auch überfrachte ich eine Vektor-Klasse nur ungerne mit Funktionen, die nur Brechnungen mit dem Datentyp darstellen und keine Grundfunktionen des Typs sind, wie Konstruktion oder Zugriff auf ein Element (einzige Ausnahme: Zusammengesetzte Operatoren wie += ).

    Auch += kann als freie Funktion implementiert werden.



  • camper schrieb:

    Auch += kann als freie Funktion implementiert werden.

    Ist mir ehrlich gesagt noch nie in den Sinn gekommen, weil sich die Member-Funktion immer so aufgedrängt hat. Daher hab ich mich noch nie gefragt, ob es wohl geht.
    Gut zu wissen... obwohl mir das wahrscheinlich ein wenig wie OOP in C vorkäme: eine freie Funktion, der man einen "this"-Pointer zum modifizieren übergibt 😉



  • Vector2D schrieb:

    Generelle Empfehlungen bei solchen Funktion?

    wenn es keinen Grund für andere Entscheidung gibt, würde ich non-Member-Funktionen bevorzugen. Non-Member-Funktionen haben keinen Zugriff (sofern die Grenze nicht mit "friend" aufgeweicht wird) auf implementationsspezifische Interna der Klasse, auf die sie zugreifen, und unterstützen damit das OOP-Prinzip der Datenkapselung. Wer den Code später mal refaktorieren muß, wird es womöglich danken.



  • PS: nimm doubles statt floats, die sind m.E.n. auf AMD64 schneller.

    sind sie das? Hast du dazu eine Quelle? Würde mich interessieren.
    Ich dachte immer, dass auf x86 einfach die 80bit Register für Fließkommaberechnungen genommen werden, d.h. es sollte keinen Unterschied machen ob ich da jetzt eine float (32) oder double (64) reinlade und dort rechne.
    Beim Speichern kann wiederum float von Vorteil sein, wenn ich ein Mesh mit Millionen Vertices habe, dann bietet sich float an um Speicher zu sparen, double hingegen für Berechnungen und temporäre Ergebnisse.


  • Mod

    hghgfhfh schrieb:

    PS: nimm doubles statt floats, die sind m.E.n. auf AMD64 schneller.

    sind sie das? Hast du dazu eine Quelle? Würde mich interessieren.
    Ich dachte immer, dass auf x86 einfach die 80bit Register für Fließkommaberechnungen genommen werden, d.h. es sollte keinen Unterschied machen ob ich da jetzt eine float (32) oder double (64) reinlade und dort rechne.
    Beim Speichern kann wiederum float von Vorteil sein, wenn ich ein Mesh mit Millionen Vertices habe, dann bietet sich float an um Speicher zu sparen, double hingegen für Berechnungen und temporäre Ergebnisse.

    Das sind alte Faustregeln, aus den Zeiten, als sowieso alles über die FPU ging, wodurch double der native Datentyp der Maschine war und somit mindestens genau so schnell wie float, wenn nicht gar minimal schneller, bei wesentlich höherer Genauigkeit (double ist für alle Belange der echten Welt genau genug, mit float hat man tatsächlich schnell mal Probleme). Diese Überlegung stimmt jedoch nicht mehr mit neueren Rechenwerken wie SSE, die gegebenenfalls zwei floats gleichzeitig anstelle eines doubles verarbeiten können. Und ganz bestimmt gilt die Überlegung nicht auf Hardware, auf der float der native Datentyp ist; das sind beispielsweise sämtliche Grafikkarten für den Heimanwenderbereich.



  • hghgfhfh schrieb:

    Ich dachte immer, dass auf x86 einfach die 80bit Register für Fließkommaberechnungen genommen werden

    Modernere Compiler benutzen dafür meist die SSE-Befehle und -Register, gerade bei AMD64. Die FPU ist zwar verfügbar, aber wird selten genutzt.

    hghgfhfh schrieb:

    PS: nimm doubles statt floats, die sind m.E.n. auf AMD64 schneller.

    sind sie das? Hast du dazu eine Quelle? Würde mich interessieren.

    Mich auch. Ich wüßte nicht, daß skalare Operationen mit Floats oder Doubles schneller sein sollten. Ich weiß aber, daß moderne Compiler oft und gerne skalare Instruktionen vektorisieren. Und für vektorisierbare Operationen ist der potentielle Durchsatz bei float - doppelt so hoch wie bei double -Skalaren.

    Evtl. gab es bei der x87-FPU einen Overhead für das Laden und Speichern von 32-Bit-Floats im Vergleich zu 64-Bit-Floats, was den Eindruck "die sind schneller" ausmachen könnte. (Meines Wissens war der native Datentyp der FPU nicht 64, sondern 80 Bits breit.) Aber SSE kann nativ mit 32-Bit- und 64-Bit-Floats rechnen.

    SeppJ schrieb:

    double ist für alle Belange der echten Welt genau genug

    Leider nicht 🙂 Aber für deutlich mehr als float .


  • Mod

    audacia|off schrieb:

    SeppJ schrieb:

    double ist für alle Belange der echten Welt genau genug

    Leider nicht 🙂 Aber für deutlich mehr als float .

    Da bin ich mal gespannt, wo du eine Genauigkeit von mehr als 10^-15 zu brauchen meinst.

    Mal als Vergleichswert: Die genauesten Messungen überhaupt von irgendetwas auf der Welt haben eine Genauigkeit von gut 10^-14.



  • hghgfhfh schrieb:

    sind sie das? Hast du dazu eine Quelle? Würde mich interessieren.

    Hmmm, da habe ich mich wohl getäuscht. 🙂
    Ich hatte in Erinnerung, dass ich das selbst mal festgestellt hatte bei einem Projekt. Da war wohl noch etwas anderes involviert, was ich nicht berücksichtigt habe.
    In der Realität ist wohl float schneller, wegen kleinerem Speicherverbrauch und ergo weniger Cache Misses und wegen SIMD (wie schon SeppJ sagte).



  • SeppJ schrieb:

    Da bin ich mal gespannt, wo du eine Genauigkeit von mehr als 10^-15 zu brauchen meinst.

    Mehr Genauigkeit in der Mantisse brauche ich nicht. Aber ich bin aber einmal an die Limits des Exponenten-Wertebereichs gestoßen. Das war bei einer numerischen Implementierung der Hopf-Cole-Transformation.


Log in to reply