PDA

Archiv verlassen und diese Seite im Standarddesign anzeigen : Vektormathematik optimieren (C++)


Neomi
2006-05-02, 14:51:43
Am Wochenende habe ich ein klein wenig an Vektoren und Matrizen geschraubt, aber eine Sache wurmt mich doch sehr. Ich habe einen Zwischentyp, der für deutlich mehr Flexibilität sorgt, aber er kostet Performance, die eigentlich nicht verloren gehen dürfte. In Visual C++ 200x (habe mit 2002, 2003 und 2005 getestet) greift der Optimizer wohl ins Klo.

Hier ist der deutlich zusammengeschrumpfte (alles unnötige raus, sogar Namespaces) Problemfall:

#include <windows.h>
#include <stdio.h>
#include <tchar.h>

#pragma inline_depth (255)
#pragma inline_recursion (on)
#pragma auto_inline (on)
#define inline __forceinline

struct float3
{
float f0, f1, f2;

inline float3 () {}
inline float3 (const float f0, const float f1, const float f2) : f0 (f0), f1 (f1), f2 (f2) {}
};

union Vector3
{
struct
{
float x;
float y;
float z;
};

float m [3];

inline Vector3 () {}
inline Vector3 (const float x, const float y, const float z) : x (x), y (y), z (z) {}
inline Vector3 (const float3 & f) : x (f.f0), y (f.f1), z (f.f2) {}

inline const Vector3 & operator = (const float3 & f) { x = f.f0; y = f.f1; z = f.f2; return (*this); }

inline operator const float3 () const { return (float3 (x, y, z)); }
};

inline const float3 operator + (const float3 & Op1, const float3 & Op2)
{
return (float3 (Op1.f0 + Op2.f0, Op1.f1 + Op2.f1, Op1.f2 + Op2.f2));
}

#pragma auto_inline (off) // don't inline these functions

const float3 Test (const float3 & v1, const float3 & v2, const float3 & v3, const float3 & v4,
const float3 & v5, const float3 & v6, const float3 & v7, const float3 & v8)
{
return (v1 + v2 + v3 + v4 + v5 + v6 + v7 + v8);
}

const Vector3 Test (const Vector3 & v1, const Vector3 & v2, const Vector3 & v3, const Vector3 & v4,
const Vector3 & v5, const Vector3 & v6, const Vector3 & v7, const Vector3 & v8)
{
return (v1 + v2 + v3 + v4 + v5 + v6 + v7 + v8);
}

#pragma auto_inline () // automatic inlining back to default

void DebugMsg (TCHAR * pszMsg, ...);

int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow)
{
LARGE_INTEGER start, end, freq;
float3 v1, v2, v3, v4, v5, v6, v7, v8; // as fast as it can get?
// Vector3 v1, v2, v3, v4, v5, v6, v7, v8; // definitely too slow
Vector3 v;
int i, j;

v1 = Vector3 (1.0f, 1.25f, 1.5f);
v2 = Vector3 (2.0f, 2.25f, 2.5f);
v3 = Vector3 (3.0f, 3.25f, 3.5f);
v4 = Vector3 (4.0f, 4.25f, 4.5f);
v5 = Vector3 (5.0f, 5.25f, 5.5f);
v6 = Vector3 (6.0f, 6.25f, 6.5f);
v7 = Vector3 (7.0f, 7.25f, 7.5f);
v8 = Vector3 (8.0f, 8.25f, 8.5f);

for (i = 0; i < 5; i++)
{
QueryPerformanceCounter (&start);

for (j = 0; j < 10000000; j++)
v = Test (v1, v2, v3, v4, v5, v6, v7, v8);

QueryPerformanceCounter (&end);
QueryPerformanceFrequency (&freq);

DebugMsg (TEXT ("Time: %.5f"), (double) (end.QuadPart - start.QuadPart) / (double) freq.QuadPart);
}

DebugMsg (TEXT ("x:\t%.5f\ny:\t%.5f\nz:\t%.5f"), v.x, v.y, v.z);

return (0);
}

void DebugMsg (TCHAR * pszMsg, ...)
{
static TCHAR szMsg [256];
va_list vaArgs;

if ((pszMsg == NULL) || (*pszMsg == '\0'))
return;

va_start (vaArgs, pszMsg);

if (_vsntprintf (szMsg, 255, pszMsg, vaArgs) == -1)
_tcscpy (&szMsg [252], TEXT ("..."));

va_end (vaArgs);

MessageBox (NULL, szMsg, TEXT ("Debug"), MB_OK);

return;
}

Der Unterschied, um den es mir geht, läßt sich am Anfang der WinMain erzwingen, indem die Deklaration einiger Vektoren von float3 auf Vector3 umgestellt wird:

// float3 v1, v2, v3, v4, v5, v6, v7, v8; // as fast as it can get?
Vector3 v1, v2, v3, v4, v5, v6, v7, v8; // definitely too slow

Eigentlich sollte es keinen Unterschied geben, aber Vector3 ist deutlich langsamer und benötigt mehr Stack. Mit Expression Templates komme ich zwar etwa auf die gleiche Geschwindigkeit, benötige aber trotzdem noch mehr Stack. Und da per Templates serialisierte Berechnungen bei der Erzeugung von SSE-Code hinderlich sein können und andere Compiler vielleicht wieder anders reagieren, möchte ich mehrere Varianten wahlweise nutzbar (per #define) vorhalten. Auf die Zwischenstruktur (hier nur float3) möchte ich nur sehr ungerne verzichten, weil ich nur darüber wirklich frei swizzlen kann (z.B. von einem Vector2 per .xxyy hoch zu einem Vector4) oder Vektoren mit Farbwerten verrechnen, ohne eine ganze Legion an überladenen Funktionen bauen zu müssen (hab schon für alle Kombinationen von float1 bis float4 ganze 64 Varianten für z.B. Clamp).

Hat irgendwer eine Idee (ich habe keine funktionierende), wie ich die Vector3-Rechnung auf die Geschwindigkeit und den Stackverbrauch der float3-Rechnung bekommen kann, ohne auf den float3-Umweg verzichten zu müssen?

Corrail
2006-05-02, 17:25:30
Hast du alle Optimierungen eingeschalten? weil ich weiß nicht, in wieweit MSVC den ausdruck
v1 + v2 + v3 + v4 + v5 + v6 + v7 + v8
optimieren kann. Bei floats ist das kein Problem, aber bei Vector3 werden im schlimmsten Fall 7 temporäre Objekte erstellt und zerstört. Probier mal:

Vector3 tmp(v1+v2);
tmp+=v3;
tmp+=v4;
tmp+=v5;
tmp+=v6;
tmp+=v7;
tmp+=v8;


Ist zwar überhaupt nicht hübsch, würde aber mal klären, ob es an den temporären Objekten liegt. Zu der ganzen Sache mit Klassen usw.: Wir haben uns mal einen Integer selbst geschrieben in einer Klasse gewrappt. Wir wollte dadurch dem Interger mehr Funktionalität geben (vorallem fürs Metaprogramming: member typedefs). Linux/gcc schafft es, diese Integer Klasse so zu optimieren, dass der Speed einem Standard-int gleich kommt.

[EDIT] Ich weiß nicht genau, ab welcher Datenmenge es sinnvoll ist, aber eventuell ist ein memcpy() schneller als ein normaler Copy Constructor.

Neomi
2006-05-02, 18:00:37
Corrail[/POST]']Hast du alle Optimierungen eingeschalten?

Jep. Alles, was Visual Studio an Codebeschleunigern anbietet, ist drin. Die Pragmas und __forceinline sorgen für den Rest, bzw. sollten dafür sorgen.

Corrail[/POST]']Bei floats ist das kein Problem, aber bei Vector3 werden im schlimmsten Fall 7 temporäre Objekte erstellt und zerstört.

Die Erstellung der temporären Objekte wird ja gut rausoptimiert, wenn ich direkt mit (nicht serialisierten) float3-Strukturen rechne. Wenn ich mit Vector3 rechne, werden ebenfalls float3 ausgespuckt, nur eben noch mit implizitem Casting dabei. Die Strukturen, die dabei dann entstehen, werden wohl nicht mehr sauber aufgelöst, der Code ist gerade mal halb so schnell (eher langsamer, da der Overhead durch den Funktionsausruf in beiden Varianten gleich ist). Deine Variante mit += bringt zwar ein klein wenig Geschwindigkeit, ist aber noch sehr weit von der float3-Rechnung entfernt.

Chris Lux
2006-05-02, 20:01:24
hi,
hast du mal ausprobiert welcher operator die meiste zeit verbrät. ich meine den = operator von Vector3 oder der copy constructor. kommentiere die doch mal aus und teste mit den automatisch erzeugten.

wie hast du das swizzeling denn umgesetzt, das würde mich mal interessieren? nur wenn es eben eine kolonne an methoden ist möcht ichs nicht wissen ;)

zu deiner union. ich habe sowas ähnliches für meine vector klassen gemacht. aber das ist glaube besser wenn du das doch in einer klasse oder einem struct kapselst:


template<typename scm_scalar>
class vec<scm_scalar, 2>
{
public:
vec() {}
vec(const vec<scm_scalar, 2>& v) : x(v.x), y(v.y) {}
explicit vec(const scm_scalar s) : x(s), y(s) {}
explicit vec(const scm_scalar s, const scm_scalar t) : x(s), y(t) {}

scm_scalar& operator[](const unsigned i) { assert(i < 2); return (vec_array[i]); }
const scm_scalar& operator[](const unsigned i) const { assert(i < 2); return (vec_array[i]); }

// data definition
union
{
struct
{
scm_scalar x;
scm_scalar y;
};
scm_scalar vec_array[2];
};
}; // class vec2


die operatoren und funktionen habe ich dann wie du als funktionen gebastelt. find ich einfach übersichtlicher.

Coda
2006-05-02, 22:20:00
Hast du schonmal das Disassemblat verglichen?

Neomi
2006-05-02, 23:12:34
Chris Lux[/POST]']hast du mal ausprobiert welcher operator die meiste zeit verbrät. ich meine den = operator von Vector3 oder der copy constructor. kommentiere die doch mal aus und teste mit den automatisch erzeugten.

Mit dem automatisch generierten kann ich schlecht testen, weil ich ja bei der Zuweisung von float3 zu Vector3 umwandeln muß.

Chris Lux[/POST]']wie hast du das swizzeling denn umgesetzt, das würde mich mal interessieren? nur wenn es eben eine kolonne an methoden ist möcht ichs nicht wissen ;)

Eine Kolonne ist es nur indirekt (über Makros). Ich habe das so gebaut, daß ich pro Swizzlemaske eine einzelne Zeile habe, was dann beim Vector4 auch nicht mehr wenig ist. Hier mal meine Vector2.h als Beispiel:

#ifndef _INC_NMATH_VECTOR2_H_
#define _INC_NMATH_VECTOR2_H_

#include "FloatX.h"

#ifdef _NMATH_ENABLE_SWIZZLING_

// low level macros

#ifndef _NMATH_SERIALIZE_VECTORS_

#define Vector2_AssignOps(Op,Index0,Index1) \
inline const FloatX::float2 operator Op (const FloatX::float1 & f) { return (FloatX::float2 (m [Index0] Op f.f0, m [Index1] Op f.f0)); } \
inline const FloatX::float2 operator Op (const FloatX::float2 & f) { return (FloatX::float2 (m [Index0] Op f.f0, m [Index1] Op f.f1)); } \
inline const FloatX::float2 operator Op (const FloatX::float3 & f) { return (FloatX::float2 (m [Index0] Op f.f0, m [Index1] Op f.f1)); } \
inline const FloatX::float2 operator Op (const FloatX::float4 & f) { return (FloatX::float2 (m [Index0] Op f.f0, m [Index1] Op f.f1)); }

#define Vector2_CastingOp(Index0,Index1) \
inline operator const FloatX::float2 () const { return (FloatX::float2 (m [Index0], m [Index1])); }

#define Vector3_CastingOp(Index0,Index1,Index2) \
inline operator const FloatX::float3 () const { return (FloatX::float3 (m [Index0], m [Index1], m [Index2])); }

#define Vector4_CastingOp(Index0,Index1,Index2,Index3) \
inline operator const FloatX::float4 () const { return (FloatX::float4 (m [Index0], m [Index1], m [Index2], m [Index3])); }

#else // _NMATH_SERIALIZE_VECTORS_

#define Vector2_AssignOps(Op,Index0,Index1) \
inline const FloatX::float2 operator Op (const FloatX::float2 & f) { return (FloatX::float2 (m [Index0] Op f.f0, m [Index1] Op f.f1)); }

#define Vector2_CastingOp(Index0,Index1) \
inline operator const FloatX::float2 () const { return (FloatX::float2 (m [Index0], m [Index1])); } \
inline const float Eval (const int i) const { return (m [(i == 0) ? Index0 : Index1]); } \
inline const int Dim () const { return (2); }

#define Vector3_CastingOp(Index0,Index1,Index2) \
inline operator const FloatX::float3 () const { return (FloatX::float3 (m [Index0], m [Index1], m [Index2])); } \
inline const float Eval (const int i) const { return (m [(i == 0) ? Index0 : ((i == 1) ? Index1 : Index2)]); } \
inline const int Dim () const { return (3); }

#define Vector4_CastingOp(Index0,Index1,Index2,Index3) \
inline operator const FloatX::float4 () const { return (FloatX::float4 (m [Index0], m [Index1], m [Index2], m [Index3])); } \
inline const float Eval (const int i) const { return (m [(i == 0) ? Index0 : ((i == 1) ? Index1 : ((i == 2) ? Index2 : Index3))]); } \
inline const int Dim () const { return (4); }

#endif // _NMATH_SERIALIZE_VECTORS_

// read/write and read only masks

#define Vector2_Comp2_RW(Name,Index0,Index1) \
struct _##Name \
{ \
private: \
float m [2]; \
\
public: \
Vector2_AssignOps ( =, Index0, Index1); \
Vector2_AssignOps (+=, Index0, Index1); \
Vector2_AssignOps (-=, Index0, Index1); \
Vector2_AssignOps (*=, Index0, Index1); \
Vector2_AssignOps (/=, Index0, Index1); \
Vector2_CastingOp (Index0, Index1); \
} Name

#define Vector2_Comp2_RO(Name,Index0,Index1) \
struct _##Name \
{ \
private: \
float m [2]; \
\
public: \
Vector2_CastingOp (Index0, Index1); \
} Name

#define Vector2_Comp3_RO(Name,Index0,Index1,Index2) \
struct _##Name \
{ \
private: \
float m [2]; \
\
public: \
Vector3_CastingOp (Index0, Index1, Index2); \
} Name

#define Vector2_Comp4_RO(Name,Index0,Index1,Index2,Index3) \
struct _##Name \
{ \
private: \
float m [2]; \
\
public: \
Vector4_CastingOp (Index0, Index1, Index2, Index3); \
} Name

#endif // _NMATH_ENABLE_SWIZZLING_

// vector structure

union Vector2
{
struct
{
float x;
float y;
};

float m [2];

inline Vector2 () {}
inline Vector2 (const float x, const float y) : x (x), y (y) {}

#ifndef _NMATH_SERIALIZE_VECTORS_
inline Vector2 (const FloatX::float1 & f) : x (f.f0), y (f.f0) {}
inline Vector2 (const FloatX::float2 & f) : x (f.f0), y (f.f1) {}
inline Vector2 (const FloatX::float3 & f) : x (f.f0), y (f.f1) {}
inline Vector2 (const FloatX::float4 & f) : x (f.f0), y (f.f1) {}

inline const Vector2 & operator = (const FloatX::float1 & f) { x = f.f0; y = f.f0; return (*this); }
inline const Vector2 & operator = (const FloatX::float2 & f) { x = f.f0; y = f.f1; return (*this); }
inline const Vector2 & operator = (const FloatX::float3 & f) { x = f.f0; y = f.f1; return (*this); }
inline const Vector2 & operator = (const FloatX::float4 & f) { x = f.f0; y = f.f1; return (*this); }
#else // _NMATH_SERIALIZE_VECTORS_
inline Vector2 (const FloatX::float2 & f) : x (f.f0), y (f.f1) {}

inline const Vector2 & operator = (const FloatX::float2 & f) { x = f.f0; y = f.f1; return (*this); }
#endif // _NMATH_SERIALIZE_VECTORS_

inline operator const FloatX::float2 () const { return (FloatX::float2 (x, y)); }

#ifdef _NMATH_ENABLE_SWIZZLING_
// swizzled component access

Vector2_Comp2_RO (xx, 0, 0);
Vector2_Comp2_RW (xy, 0, 1);
Vector2_Comp2_RW (yx, 1, 0);
Vector2_Comp2_RO (yy, 1, 1);
Vector2_Comp3_RO (xxx, 0, 0, 0);
Vector2_Comp3_RO (xxy, 0, 0, 1);
Vector2_Comp3_RO (xyx, 0, 1, 0);
Vector2_Comp3_RO (xyy, 0, 1, 1);
Vector2_Comp3_RO (yxx, 1, 0, 0);
Vector2_Comp3_RO (yxy, 1, 0, 1);
Vector2_Comp3_RO (yyx, 1, 1, 0);
Vector2_Comp3_RO (yyy, 1, 1, 1);
Vector2_Comp4_RO (xxxx, 0, 0, 0, 0);
Vector2_Comp4_RO (xxxy, 0, 0, 0, 1);
Vector2_Comp4_RO (xxyx, 0, 0, 1, 0);
Vector2_Comp4_RO (xxyy, 0, 0, 1, 1);
Vector2_Comp4_RO (xyxx, 0, 1, 0, 0);
Vector2_Comp4_RO (xyxy, 0, 1, 0, 1);
Vector2_Comp4_RO (xyyx, 0, 1, 1, 0);
Vector2_Comp4_RO (xyyy, 0, 1, 1, 1);
Vector2_Comp4_RO (yxxx, 1, 0, 0, 0);
Vector2_Comp4_RO (yxxy, 1, 0, 0, 1);
Vector2_Comp4_RO (yxyx, 1, 0, 1, 0);
Vector2_Comp4_RO (yxyy, 1, 0, 1, 1);
Vector2_Comp4_RO (yyxx, 1, 1, 0, 0);
Vector2_Comp4_RO (yyxy, 1, 1, 0, 1);
Vector2_Comp4_RO (yyyx, 1, 1, 1, 0);
Vector2_Comp4_RO (yyyy, 1, 1, 1, 1);
#endif // _NMATH_ENABLE_SWIZZLING_
};

// undefine macros

#ifdef _NMATH_ENABLE_SWIZZLING_

#undef Vector2_AssignOps
#undef Vector2_CastingOp
#undef Vector3_CastingOp
#undef Vector4_CastingOp
#undef Vector2_Comp2_RW
#undef Vector2_Comp2_RO
#undef Vector2_Comp3_RO
#undef Vector2_Comp4_RO

#endif // _NMATH_ENABLE_SWIZZLING_

// assignment operators

inline const Vector2 & operator += (Vector2 & Dst, const FloatX::float2 & f)
{
Dst.x += f.f0;
Dst.y += f.f1;

return (Dst);
}

inline const Vector2 & operator -= (Vector2 & Dst, const FloatX::float2 & f)
{
Dst.x -= f.f0;
Dst.y -= f.f1;

return (Dst);
}

inline const Vector2 & operator *= (Vector2 & Dst, const FloatX::float2 & f)
{
Dst.x *= f.f0;
Dst.y *= f.f1;

return (Dst);
}

inline const Vector2 & operator /= (Vector2 & Dst, const FloatX::float2 & f)
{
Dst.x /= f.f0;
Dst.y /= f.f1;

return (Dst);
}

#ifndef _NMATH_SERIALIZE_VECTORS_

inline const Vector2 & operator += (Vector2 & Dst, const FloatX::float1 & f) { return (Dst += FloatX::float2 (f.f0, f.f0)); }
inline const Vector2 & operator += (Vector2 & Dst, const FloatX::float3 & f) { return (Dst += FloatX::float2 (f.f0, f.f1)); }
inline const Vector2 & operator += (Vector2 & Dst, const FloatX::float4 & f) { return (Dst += FloatX::float2 (f.f0, f.f1)); }

inline const Vector2 & operator -= (Vector2 & Dst, const FloatX::float1 & f) { return (Dst -= FloatX::float2 (f.f0, f.f0)); }
inline const Vector2 & operator -= (Vector2 & Dst, const FloatX::float3 & f) { return (Dst -= FloatX::float2 (f.f0, f.f1)); }
inline const Vector2 & operator -= (Vector2 & Dst, const FloatX::float4 & f) { return (Dst -= FloatX::float2 (f.f0, f.f1)); }

inline const Vector2 & operator *= (Vector2 & Dst, const FloatX::float1 & f) { return (Dst *= FloatX::float2 (f.f0, f.f0)); }
inline const Vector2 & operator *= (Vector2 & Dst, const FloatX::float3 & f) { return (Dst *= FloatX::float2 (f.f0, f.f1)); }
inline const Vector2 & operator *= (Vector2 & Dst, const FloatX::float4 & f) { return (Dst *= FloatX::float2 (f.f0, f.f1)); }

inline const Vector2 & operator /= (Vector2 & Dst, const FloatX::float1 & f) { return (Dst /= FloatX::float2 (f.f0, f.f0)); }
inline const Vector2 & operator /= (Vector2 & Dst, const FloatX::float3 & f) { return (Dst /= FloatX::float2 (f.f0, f.f1)); }
inline const Vector2 & operator /= (Vector2 & Dst, const FloatX::float4 & f) { return (Dst /= FloatX::float2 (f.f0, f.f1)); }

#else // _NMATH_SERIALIZE_VECTORS_

namespace FloatX
{
template <>
struct FloatX_Arg <Vector2>
{
const Vector2 & Arg;

inline FloatX_Arg (const Vector2 & Arg) : Arg (Arg) {}

inline const float Eval (const int i) const { return (Arg.m [(i <= 1) ? i : 1]); }
inline const int Dim () const { return (2); }
};
}

#endif // _NMATH_SERIALIZE_VECTORS_

#endif // _INC_NMATH_VECTOR2_H_

Wenn _NMATH_SERIALIZE_VECTORS_ definiert ist (vor dem Inkludieren der NMath.h, die diese Bibliothek nach außen hin nutzbar macht), läuft alles (mathematisch äquivalent und mit weniger Code) über Expression Templates. Sogar Funktionen wie Clamp und Lerp werden über Templates serialisiert. Das habe ich deshalb nur wahlweise, weil es eben nicht in jeder Situation besser ist (SSE z.B.) und evtl. nicht jedem Compiler schmeckt. Ich bin da gerne flexibel.

Da das Swizzling dank der nicht gerade kleinen Makros doch eine ganze Menge Holz für den Compiler ist, habe ich das auch nur optional drin, falls _NMATH_ENABLE_SWIZZLING_ definiert ist.Dadurch kann ich in jeder Sourcendatei bestimmen, ob ich Swizzling brauche oder nicht. Und wenn nicht, dann wird ohne diese "Ballast" recht zügig compiliert. Swizzling habe ich sogar in den einzelnen Zeilen und Spalten (auf die ich per row[] und col[] zugreifen kann) meiner Matrizen implementiert.

Rein vom Konzept her funktioniert das alles ziemlich gut. Nur die Performance stört mich. Ich weiß, daß es schneller geht, nur kann ich den Compiler von Visual Studio nicht so recht davon überzeugen. Und da ich Perfektionist bin, bin ich nicht wirklich zufrieden, solange ich nicht das rausgeholt habe, was ich für machbar halte.

Chris Lux[/POST]']zu deiner union. ich habe sowas ähnliches für meine vector klassen gemacht. aber das ist glaube besser wenn du das doch in einer klasse oder einem struct kapselst:

Zuerst hatte ich das auch in einer Struktur, aber das ist kein echter Unterschied zu einer Union. Wenn man es nicht innen "versteckt", sondern gleich als Union schreibt, ist es nur ein klein wenig kürzer. Geschmackssache eben.

Coda[/POST]']Hast du schonmal das Disassemblat verglichen?

Jep, darüber habe ich dann das mit dem Stackverbrauch bemerkt. Darin finde ich aber nichts, was auf eine Lösung hindeutet. Nur Dinge, die der Compiler eigentlich noch rausoptimieren sollte, was er aber nicht tut.

Neomi
2006-05-02, 23:31:51
Vielleicht kannst du (oder jemand anderes) ja was daraus erkennen. Assemblerhabe ich zwar schon länger nicht mehr genutzt, kann es aber noch lesen und schreiben. Ich sehe zwar deutlich, was daneben geht, aber nicht, was ich im Quelltext ändern muß, um das zu verhindern.

Die schnelle Variante über float3:
const float3 Test (const float3 & v1, const float3 & v2, const float3 & v3, const float3 & v4,
const float3 & v5, const float3 & v6, const float3 & v7, const float3 & v8)
{
00409880 push ebx
00409881 mov ebx,dword ptr [esp+18h]
00409885 push ebp
return (v1 + v2 + v3 + v4 + v5 + v6 + v7 + v8);
00409886 mov ebp,dword ptr [esp+14h]
0040988A fld dword ptr [ebp]
0040988D mov ebp,dword ptr [esp+10h]
00409891 fadd dword ptr [ebp]
00409894 mov eax,dword ptr [esp+0Ch]
00409898 fld dword ptr [ebp+4]
0040989B mov ebp,dword ptr [esp+14h]
0040989F fadd dword ptr [ebp+4]
004098A2 mov ebp,dword ptr [esp+10h]
004098A6 fld dword ptr [v1]
004098A9 mov ebp,dword ptr [esp+14h]
004098AD fadd dword ptr [v1]
004098B0 mov ebp,dword ptr [esp+18h]
004098B4 fld dword ptr [ebp]
004098B7 faddp st(3),st
004098B9 fld dword ptr [ebp+4]
004098BC faddp st(2),st
004098BE fadd dword ptr [v1]
004098C1 pop ebp
004098C2 fld dword ptr [ebx]
004098C4 faddp st(3),st
004098C6 fld dword ptr [ebx+4]
004098C9 faddp st(2),st
004098CB fadd dword ptr [ebx+8]
004098CE pop ebx
004098CF fld dword ptr [edi]
004098D1 faddp st(3),st
004098D3 fld dword ptr [edi+4]
004098D6 faddp st(2),st
004098D8 fadd dword ptr [edi+8]
004098DB fld dword ptr [esi]
004098DD faddp st(3),st
004098DF fld dword ptr [esi+4]
004098E2 faddp st(2),st
004098E4 fadd dword ptr [esi+8]
004098E7 fld dword ptr [edx]
004098E9 faddp st(3),st
004098EB fld dword ptr [edx+4]
004098EE faddp st(2),st
004098F0 fadd dword ptr [edx+8]
004098F3 fld dword ptr [ecx]
004098F5 faddp st(3),st
004098F7 fxch st(2)
004098F9 fstp dword ptr [eax]
004098FB fadd dword ptr [ecx+4]
004098FE fstp dword ptr [eax+4]
00409901 fadd dword ptr [ecx+8]
00409904 fstp dword ptr [eax+8]
}
00409907 ret

Und per Vector3, zu langsam:
const Vector3 Test (const Vector3 & v1, const Vector3 & v2, const Vector3 & v3, const Vector3 & v4,
const Vector3 & v5, const Vector3 & v6, const Vector3 & v7, const Vector3 & v8)
{
00409880 sub esp,54h
return (v1 + v2 + v3 + v4 + v5 + v6 + v7 + v8);
00409883 fld dword ptr [ecx]
00409885 push ebx
00409886 fld dword ptr [ecx+4]
00409889 mov ebx,dword ptr [esp+6Ch]
0040988D fld dword ptr [ecx+8]
00409890 mov ecx,dword ptr [esp+64h]
00409894 fld dword ptr [edx]
00409896 push ebp
00409897 fld dword ptr [edx+4]
0040989A mov ebp,dword ptr [esp+6Ch]
0040989E fld dword ptr [edx+8]
004098A1 mov eax,dword ptr [esp+60h]
004098A5 fld dword ptr [esi]
004098A7 fstp dword ptr [esp+44h]
004098AB fld dword ptr [esi+4]
004098AE fstp dword ptr [esp+48h]
004098B2 fld dword ptr [esi+8]
004098B5 fstp dword ptr [esp+4Ch]
004098B9 fld dword ptr [edi]
004098BB fstp dword ptr [esp+38h]
004098BF fld dword ptr [edi+4]
004098C2 fstp dword ptr [esp+3Ch]
004098C6 fld dword ptr [edi+8]
004098C9 fstp dword ptr [esp+40h]
004098CD fld dword ptr [ebx]
004098CF fstp dword ptr [esp+2Ch]
004098D3 fld dword ptr [ebx+4]
004098D6 fstp dword ptr [esp+30h]
004098DA fld dword ptr [ebx+8]
004098DD fstp dword ptr [esp+34h]
004098E1 fld dword ptr [ebp]
004098E4 fstp dword ptr [esp+20h]
004098E8 fld dword ptr [ebp+4]
004098EB fstp dword ptr [esp+24h]
004098EF fld dword ptr [v1]
004098F2 fstp dword ptr [esp+28h]
004098F6 fld dword ptr [ecx]
004098F8 fld dword ptr [ecx+4]
004098FB fstp dword ptr [esp+54h]
004098FF fld dword ptr [ecx+8]
00409902 mov ecx,dword ptr [esp+64h]
00409906 fstp dword ptr [esp+58h]
0040990A fld dword ptr [ecx]
0040990C fstp dword ptr [esp+8]
00409910 fld dword ptr [ecx+4]
00409913 fstp dword ptr [esp+0Ch]
00409917 fld dword ptr [ecx+8]
0040991A fstp dword ptr [esp+10h]
0040991E fadd dword ptr [esp+8]
00409922 fstp dword ptr [esp+14h]
00409926 fld dword ptr [esp+0Ch]
0040992A fadd dword ptr [esp+54h]
0040992E fstp dword ptr [esp+18h]
00409932 fld dword ptr [esp+10h]
00409936 fadd dword ptr [esp+58h]
0040993A fstp dword ptr [esp+1Ch]
0040993E fld dword ptr [esp+14h]
00409942 fadd dword ptr [esp+20h]
00409946 fstp dword ptr [esp+8]
0040994A fld dword ptr [esp+18h]
0040994E fadd dword ptr [esp+24h]
00409952 fstp dword ptr [esp+0Ch]
00409956 fld dword ptr [esp+1Ch]
0040995A fadd dword ptr [esp+28h]
0040995E fstp dword ptr [esp+10h]
00409962 fld dword ptr [esp+8]
00409966 fadd dword ptr [esp+2Ch]
0040996A fstp dword ptr [esp+20h]
0040996E fld dword ptr [esp+0Ch]
00409972 fadd dword ptr [esp+30h]
00409976 fstp dword ptr [esp+24h]
0040997A fld dword ptr [esp+10h]
0040997E fadd dword ptr [esp+34h]
00409982 fstp dword ptr [esp+28h]
00409986 fld dword ptr [esp+20h]
0040998A fadd dword ptr [esp+38h]
0040998E fstp dword ptr [esp+2Ch]
00409992 fld dword ptr [esp+24h]
00409996 fadd dword ptr [esp+3Ch]
0040999A fstp dword ptr [esp+30h]
0040999E fld dword ptr [esp+28h]
004099A2 fadd dword ptr [esp+40h]
004099A6 pop ebp
004099A7 pop ebx
004099A8 fstp dword ptr [esp+2Ch]
004099AC fld dword ptr [esp+24h]
004099B0 fadd dword ptr [esp+3Ch]
004099B4 fstp dword ptr [esp+48h]
004099B8 fld dword ptr [esp+28h]
004099BC fadd dword ptr [esp+40h]
004099C0 fstp dword ptr [esp+4Ch]
004099C4 fld dword ptr [esp+2Ch]
004099C8 fadd dword ptr [esp+44h]
004099CC fld dword ptr [esp+48h]
004099D0 faddp st(4),st
004099D2 fld dword ptr [esp+4Ch]
004099D6 faddp st(3),st
004099D8 faddp st(1),st
004099DA fxch st(2)
004099DC faddp st(5),st
004099DE faddp st(3),st
004099E0 faddp st(1),st
004099E2 fxch st(2)
004099E4 fstp dword ptr [eax]
004099E6 fstp dword ptr [eax+4]
004099E9 fstp dword ptr [eax+8]
}
004099EC add esp,54h
004099EF ret

Der Namenlose
2006-05-02, 23:35:29
Sollte die ganze Testschleife nicht vollständig wegoptimiert werden bis auf die letzte Iteration. So weit ich das sehe gibt es ja keine Nebeneffekte. Wenn nicht, dann erkennt der Compiler das anscheinend nicht richtig und es deutet darauf hin, dass auch schon die float3 Variante nicht optimal ist. Generell lassen die Compiler gerne mal einzelne Stellen nicht ganz perfekt optimeirt zurück. Das bleibt dann mal eine nicht benutzte temporäre Variable zurück oder es wird sinnlos hin und her kopiert. Man kann ein bischen damit spielen, ob man Objekte per Referenz oder als Kopie übergibt. Manchmal hilft das, je nach dem, was der Compiler nicht auf die Reihe kriegt. Das Problem ist ja meist der Check, ob irgendwelche Seiteneffekte auftreten können, und dabei ist das übergeben von Referenzen und Pointer eher schlecht, weil dann das Objekt nicht optimiert werden kann (Wenn man nicht weiß, ob an anderer Stelle darauf zugegriffen wird, dann muß man eben immer aus dem Speicher lesen bzw. gleich schreiben und kann nicht mit Registeroptimierungen arbeiten.) Möglichweise man eine union das ganze noch schlimmer. Generell sorgen viele Cast und Copies für mehr potentiellen Overhead nach dem Optimieren. Insofern würde ich die Operationen schon auch nochmal für den Vektor3 schreiben, schon allein damit man besser versteht, was abgeht (Manchmal gibt es mehere Wege für die Casts und es ist dann anders als man denkt).

Neomi
2006-05-03, 00:59:54
Der Namenlose[/POST]']Sollte die ganze Testschleife nicht vollständig wegoptimiert werden bis auf die letzte Iteration. So weit ich das sehe gibt es ja keine Nebeneffekte. Wenn nicht, dann erkennt der Compiler das anscheinend nicht richtig und es deutet darauf hin, dass auch schon die float3 Variante nicht optimal ist.

Mit den Pragmas habe ich ja schon ausgeschlossen, daß Test() inline erweitert wird. Wenn der Compiler das darf, optimiert er übrigens die Schleife komplett raus. Das tut er auch, wenn ich den Inhalt von v nicht nachträglich noch verwender, also hat er schonmal gemerkt, daß die Funktion keine globalen Daten verändert. Richtig, eigentlich hätte ein vorgezogener Aufruf und eine Eliminierung der Schleife es "besser" getan (aus ergebnisorientierter Sicht). Hätte der Compiler diese Optimierung ausgeführt, hätte ich eben noch einen globalen Zähler einmal pro Aufruf erhöht und nachher dessen Wert ausgegeben.

Der Namenlose[/POST]']Das Problem ist ja meist der Check, ob irgendwelche Seiteneffekte auftreten können, und dabei ist das übergeben von Referenzen und Pointer eher schlecht, weil dann das Objekt nicht optimiert werden kann (Wenn man nicht weiß, ob an anderer Stelle darauf zugegriffen wird, dann muß man eben immer aus dem Speicher lesen bzw. gleich schreiben und kann nicht mit Registeroptimierungen arbeiten.) Möglichweise man eine union das ganze noch schlimmer.

Deshalb übergebe ich konstante Referenzen, das ist schon die größte (mir bekannte) Hilfe, die man dem Compiler geben kann. Es gibt aber keinen Grund, nicht in den Registern zu arbeiten, immerhin gibt es nur ein einziges Endergebnis. Kein Zwischenergebnis wird in einer Variable abgelegt, weder global noch lokal, also stehen dem Compiler alle Optimierungsmöglichkeiten offen.

Der Namenlose[/POST]']Generell sorgen viele Cast und Copies für mehr potentiellen Overhead nach dem Optimieren. Insofern würde ich die Operationen schon auch nochmal für den Vektor3 schreiben, schon allein damit man besser versteht, was abgeht (Manchmal gibt es mehere Wege für die Casts und es ist dann anders als man denkt).

Daß die Castings (und daraus resultierende Kopien) nach dem Optimieren noch Reste hinterlassen, ist bekannt, genau das ist ja mein Problem. Wenn ich die Flexibilität erhalten will, kann ich aufs Casten nicht verzichten, aber irgendwie will ich dem Compiler nahelegen, den Overhead doch noch loszuwerden. Einfach für alle Varianten die passenden Operatoren und Funktionen zu definieren, ist kein so schöner Gedanke. Wenn ich alleine schon von float, Vector2, Vector3, Vector4, Color2, Color3 und Color4 (die Sachen will ich direkt verrechnen können, evtl. kommen da noch mehr Typen zu) ausgehe, dann wird das eine lange Liste. Die Funktion Clamp z.B. hat einen zu clampenden Wert, dazu ein Minumum und ein Maximum. Also drei Parameter, die aus beliebigen Typen gemischt werden können. Also wäre ich schon bei 343 Varianten einer einzigen Funktion, und solche Funktionen gibt es mehrere. Das schlägt sich sehr negativ auf die Compilezeit nieder, wenn ich alle Varianten in eine Headerdatei packe, die letztendlich in jeder Sourcendatei inkludiert wird. Deshalb sollten es schon Castings sein. Wenn gecastet werden muß und es dazu mehrere Möglichkeiten gibt, meldet der Compiler das als Fehler, damit habe ich keine Probleme.

Der Namenlose
2006-05-03, 01:14:41
Neomi[/POST]']
Deshalb übergebe ich konstante Referenzen, das ist schon die größte (mir bekannte) Hilfe, die man dem Compiler geben kann. Es gibt aber keinen Grund, nicht in den Registern zu arbeiten, immerhin gibt es nur ein einziges Endergebnis. Kein Zwischenergebnis wird in einer Variable abgelegt, weder global noch lokal, also stehen dem Compiler alle Optimierungsmöglichkeiten offen.


Wenn du beim Funktionsaufruf ein normales Objekt als const übergibst, bedeutet es nur, da du in der Funktion 'dieses' Objekt nicht veränderst. Es könnte aber noch woanders verändert werden. Zum Beispiel in dem du das Objekt als weiteren Parameter und diesmal nicht const reference (oder einen Teil daraus) übergibt. Deshalb muß im Zweifelsfall bei jedem Zugriff neu aus dem Speicher gelesen werden. Bin nicht sicher wie die Forderungen wirklcih sind, wovon der Optimierer ausgehen darf. Normalerweise ist ein const mehr eine Beschränkung für den Programmierer als für den Optimierer, da man ja auch recht beliebiges casten erzwingen kann. Und wenn man erstmal Referenzen oder Pointer auf etwas erzeugt hat, kann der Compiler kaum noch checken, was damit passiert. Er bilck halt nicht wirklich tief, obwohl es in diesem Fall noch gerade so erkennbar sein könnte. Würde mich aber wirklcih mal interessieren, wie die Perfo ist, wenn statt Referenzen Kopien übergeben werden.

Neomi
2006-05-03, 02:17:50
Der Namenlose[/POST]']Wenn du beim Funktionsaufruf ein normales Objekt als const übergibst, bedeutet es nur, da du in der Funktion 'dieses' Objekt nicht veränderst. Es könnte aber noch woanders verändert werden. Zum Beispiel in dem du das Objekt als weiteren Parameter und diesmal nicht const reference (oder einen Teil daraus) übergibt. Deshalb muß im Zweifelsfall bei jedem Zugriff neu aus dem Speicher gelesen werden.

Wenn eine Funktion eine Variable als konstante Referenz übergeben bekommt, darf sie die nur wieder als konstante Referenz oder als Kopie weiterreichen. Das "const" sagt, daß die referenzierte Variable innerhalb der Funktion nicht verändert wird, und das beinhaltet alle untergeordneten Funktionsaufrufe. Als Ort der möglichen Änderung bleibt also nur außerhalb der Funktion und in einem anderen Thread. Um die Dinge, die außerhalb der Funktion geschehen, muß sich die Funktion nicht kümmern, also bleiben da alle Möglichkeiten offen. Andere Threads haben ihren eigenen Stack, also bleiben da nur globale Variablen, die möglicherweise geändert werden können. Um das zu kennzeichnen, gibt es "volatile", und auch dann braucht man noch Threadsynchronisation, um saubere Variableninhalte gewährleisten zu können. Fehlt das, kann der Compiler (bzw. Linker) nach eigenem Ermessen optimieren.

Der Namenlose[/POST]']Würde mich aber wirklcih mal interessieren, wie die Perfo ist, wenn statt Referenzen Kopien übergeben werden.

Bei einem "perfekten" Compiler könnte man die Sache damit nur verschlimmern, weil deutlich mehr auf den Stack geschoben wird, als nötig ist. Da der Compiler aber alles andere als perfekt ist (sonst hätte ich ja das Problem gar nicht erst), habe ich es einfach mal versucht. Und siehe da, der Compiler konnte auf einmal alles auflösen, damit hätte ich nicht gerechnet. Mit Wertübergabe frißt der Overhead beim Aufruf zwar einen ordentlichen Teil der Performance wieder auf, aber die Steigerung ist immer noch mehr als deutlich. Der Code in der Funktion selbst ist richtig gut geworden und sieht jetzt so aus:

const Vector3 Test (const Vector3 v1, const Vector3 v2, const Vector3 v3, const Vector3 v4,
const Vector3 v5, const Vector3 v6, const Vector3 v7, const Vector3 v8)
{
return (v1 + v2 + v3 + v4 + v5 + v6 + v7 + v8);
00409880 fld dword ptr [esp+4]
00409884 fadd dword ptr [esp+10h]
00409888 fld dword ptr [esp+8]
0040988C fadd dword ptr [esp+14h]
00409890 fld dword ptr [esp+0Ch]
00409894 fadd dword ptr [esp+18h]
00409898 fxch st(2)
0040989A fadd dword ptr [esp+1Ch]
0040989E fxch st(1)
004098A0 fadd dword ptr [esp+20h]
004098A4 fxch st(2)
004098A6 fadd dword ptr [esp+24h]
004098AA fxch st(1)
004098AC fadd dword ptr [esp+28h]
004098B0 fxch st(2)
004098B2 fadd dword ptr [esp+2Ch]
004098B6 fxch st(1)
004098B8 fadd dword ptr [esp+30h]
004098BC fxch st(2)
004098BE fadd dword ptr [esp+34h]
004098C2 fxch st(1)
004098C4 fadd dword ptr [esp+38h]
004098C8 fxch st(2)
004098CA fadd dword ptr [esp+3Ch]
004098CE fxch st(1)
004098D0 fadd dword ptr [esp+40h]
004098D4 fxch st(2)
004098D6 fadd dword ptr [esp+44h]
004098DA fxch st(1)
004098DC fadd dword ptr [esp+48h]
004098E0 fxch st(2)
004098E2 fadd dword ptr [esp+4Ch]
004098E6 fxch st(1)
004098E8 fadd dword ptr [esp+50h]
004098EC fxch st(2)
004098EE fadd dword ptr [esp+54h]
004098F2 fxch st(1)
004098F4 fadd dword ptr [esp+58h]
004098F8 fxch st(2)
004098FA fadd dword ptr [esp+5Ch]
004098FE fxch st(1)
00409900 fadd dword ptr [esp+60h]
00409904 fxch st(2)
00409906 fstp dword ptr [eax]
00409908 fstp dword ptr [eax+4]
0040990B fstp dword ptr [eax+8]
}
0040990E ret

Scheint so, als hätte der Compiler dieses Problem nur mit dem konstruierten Testfall. Da solche Rechnungen wohl hauptsächlich auf lokale Variablen angewendet werden (Parameter als Kopien sind ja auch lokale Variablen, nur eben von außen initialisiert), sollte dann wohl doch alles gut laufen. Konstante Referenzen sind wohl nur dann wirklich notwendig, wenn die betroffene Funktion inline erweitert wird, in dem Fall verschenkt man sonst einiges an Performance. Das hatte ich an anderer Stelle schon deutlich gespürt, deshalb hatte ich die Möglichkeit wohl gar nicht erst ausprobiert. Danke fürs Querdenken! :)

Neomi
2006-05-03, 13:32:00
Die Variante mit der Übergabe der Parameter als Wert statt Referenz abe ich jetzt auch mal in Visual Studio 2002 ausprobiert. Das Ergebnis weicht sehr von 2005 ab. Während 2005 dadurch alles auflösen konnte, klappt das unter 2002 nur, wenn ich keine Zwischenstruktur benutze. Die Variante mit float3 profitiert leicht, trotz größerem Overhead beim Funktionsaufruf wird es insgesamt nochmal schneller. Die Vector3-Variante dagegen kann unter 2002 immer noch nicht alles auflösen und wird sogar langsamer.

Sowas macht keinen Spaß. Ich will mich doch nicht programmiertechnisch massiv einschränken, weil der Compiler validen Code nicht richtig optimieren kann. Irgendwann werde ich dann wohl doch komplett überladene Funktionsgeschwader basteln müssen. Wobei nichtmal das helfen wird, wenn ich per Swizzling auf die Komponenten zugreifen will. Immerhin gibt es dann doch einen Cast und es bleiben wieder Reste übrig. Wenn ich sämtliche Swizzlemasken noch explizit in jeder möglichen Kombination verarbeiten können will, wären das immerhin je 3723875000 Kombinationen für Clamp und Lerp und je 2402500 für Min, Max und ein paar andere. Es sind nämlich gerade mal 1550 verschiedene Typen (float, float2 bis 4, Vector2 bis 4, Color2 bis 4, col[] und row[] von Matrix3x3 und Matrix4x4, sämtliche Swizzlemasken aller swizzlebaren Typen). Ein bischen zu viel für meinen Geschmack.

Der Namenlose
2006-05-03, 13:44:03
Neomi[/POST]']Wenn eine Funktion eine Variable als konstante Referenz übergeben bekommt, darf sie die nur wieder als konstante Referenz oder als Kopie weiterreichen. Das "const" sagt, daß die referenzierte Variable innerhalb der Funktion nicht verändert wird, und das beinhaltet alle untergeordneten Funktionsaufrufe. Als Ort der möglichen Änderung bleibt also nur außerhalb der Funktion und in einem anderen Thread.

Was ich meine ist sowas:

int Test(const int& p1, int& p2)
{
p2++;
return(p1 + p2);
}

int main()
{
int h=0;
return Test(h,h);
}

Das Ergebnis hier 2, da p1 und p2 das gleiche Objekt sind, und bei der Änderung von p2 sich auch p1 trotz const ändert. Deshalb ist es im Regelfall sehr schwierig zu erkennen, ob ein Objekt (oder auf eine übergeordenete Struktur), für das irgendwo mal eine Reference erzeugt wurde, verändert wird.

Neomi
2006-05-04, 14:53:23
Der Namenlose[/POST]']Das Ergebnis hier 2, da p1 und p2 das gleiche Objekt sind, und bei der Änderung von p2 sich auch p1 trotz const ändert. Deshalb ist es im Regelfall sehr schwierig zu erkennen, ob ein Objekt (oder auf eine übergeordenete Struktur), für das irgendwo mal eine Reference erzeugt wurde, verändert wird.

Sowas ist natürlich möglich, aber doch recht fieser Programmierstil. Erst dem Compiler sagen, da ändert sich nichts, ihm dann aber die Variable unter den Füßen wegziehen. Dann muß man sich auch nicht über Fehler wundern. Problematisch wird es dann, wenn man das über Umwege unbewußt tut.

Ich habe jetzt übrigens alles umgeworfen und beschränke mich auf Vektormathematik per Templates. Ist deutlich streßfreier.

Neomi
2006-05-07, 00:01:45
Eine kleine Zusatzfrage hätte ich da gerade noch. Hat zwar nichts mit Vektormathematik direkt zu tun, aber es geht ebenfalls um Optimierungen und ein neuer Thread lohnt sich dafür wohl kaum.

Erstmal der Code dazu:
#include <windows.h>
#include <stdio.h>
#include <tchar.h>
#include <math.h>

void DebugMsg (TCHAR * pszMsg, ...)
{
static TCHAR szMsg [256];
va_list vaArgs;

if ((pszMsg == NULL) || (*pszMsg == '\0'))
return;

va_start (vaArgs, pszMsg);

if (_vsntprintf_s (szMsg, 255, pszMsg, vaArgs) == -1)
_tcscpy_s (&szMsg [252], 4, TEXT ("..."));

va_end (vaArgs);

MessageBox (NULL, szMsg, TEXT ("Debug"), MB_OK);

return;
}

#pragma auto_inline (off) // don't inline this function

const float Mul0 (const float f)
{
return (f * 0.0f);
}

#pragma auto_inline () // automatic inlining back to default

int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow)
{
float f = asinf (2.0f);

f = Mul0 (f);

DebugMsg (TEXT ("f:\t%.5f"), f);

return (0);
}
const float Mul0 (const float f)
{
return (f * 0.0f);
004099E0 fld dword ptr [esp+4]
004099E4 fmul dword ptr [string L"\0" (410358h)]
}
004099EA ret

Das ist ja durchaus richtig so, weil ein NaN mit 0 multipliziert immer noch ein NaN ist. Ich würde aber gerne so optimieren lassen, daß eine Multiplikation mit 0 immer 0 ergibt, indem die Multiplikation (und den eventuellen Term, der mit 0 multipliziert wird) ganz rausgekürzt wird. Gibt es eine Möglichkeit, den Compiler von der Nichtexistenz von NaNs ausgehen zu lassen? Ich habe es mit "__assume(f==f)" (am Anfang von Mul0) und ein paar ähnlichen Konstrukten versucht, aber das hilft nicht.

Coda
2006-05-07, 00:52:10
Hast du auf /fp:fast stehen?

Neomi
2006-05-07, 02:01:57
Coda[/POST]']Hast du auf /fp:fast stehen?

Jep, ist eingestellt. Alle Optimierungen, die für mehr Geschwindigkeit sorgen können, sind an.

Eigentlich ist dieses Verhalten ja vollkommen korrekt, aber eben auch nervig. NaNs sollten so oder so nicht entstehen, wenn sauberer Code geschrieben wird, aber diese "Sicherheitsmultiplikation" kostet trotzdem Zeit. Ich werde zwar garantiert nichts explizit mit einer konstanten 0 multiplizieren, aber in genug Fällen wird das implizit passieren.

Edit:
Wenn ich einen Testfall mit einem Nicht-NaN mache, dann ist die Multiplikation zwar raus, aber eben auch nur, weil der Compiler weiß, daß es nur mit "unkritischen" Werten aufgerufen wird. Ich hatte absichtlich einen NaN als Input erzeugt, weil der Compiler bei vollkommen unbekannten Werten (wie es im praktischen Einsatz zwangsweise vorkommt) von der Möglichkeit ausgehen muß.

Xmas
2006-05-07, 13:30:12
Neomi[/POST]']Das ist ja durchaus richtig so, weil ein NaN mit 0 multipliziert immer noch ein NaN ist. Ich würde aber gerne so optimieren lassen, daß eine Multiplikation mit 0 immer 0 ergibt, indem die Multiplikation (und den eventuellen Term, der mit 0 multipliziert wird) ganz rausgekürzt wird. Gibt es eine Möglichkeit, den Compiler von der Nichtexistenz von NaNs ausgehen zu lassen? Ich habe es mit "__assume(f==f)" (am Anfang von Mul0) und ein paar ähnlichen Konstrukten versucht, aber das hilft nicht.
Bin ich zu blöd um zu verstehen warum du nicht einfach 0 zurückgibst?

Trap
2006-05-07, 15:26:30
0*inf gibt aber auch NaN, nur sagen dass kein NaN reinkommt macht es für den Compiler nicht besser.

Neomi
2006-05-07, 17:00:29
Xmas[/POST]']Bin ich zu blöd um zu verstehen warum du nicht einfach 0 zurückgibst?

Weil ich dann etliche Dinge ausmultiplizieren müßte, die ich ansonsten einfach mit einem inline expandierten Funktionsaufruf erledigen könnte. Das Beispiel oben ist zwar weltfremd, soll aber auch nur das Problem auf den Punkt bringen. Ein realistisches Beispiel (etwas ausschweifend, deshalb zum separat ausklappen):

inline void MatrixLookAt (Matrix4x4 & MatDst, const Vector3 & Eye, const Vector3 & At, const Vector3 & Up)
{
Vector3 AxisX, AxisY, AxisZ;

AxisZ = At - Eye;
AxisX = CrossProd (Up, AxisZ);
AxisY = CrossProd (AxisZ, AxisX);

AxisX = Normalize (AxisX);
AxisY = Normalize (AxisY);
AxisZ = Normalize (AxisZ);

MatDst.col [0] = Vector4 (AxisX, -DotProd (AxisX, Eye));
MatDst.col [1] = Vector4 (AxisY, -DotProd (AxisY, Eye));
MatDst.col [2] = Vector4 (AxisZ, -DotProd (AxisZ, Eye));
MatDst.col [3] = Vector4 (0.0f, 0.0f, 0.0f, 1.0f);

return;
}

inline void MatrixPerspectiveFov (Matrix4x4 & MatDst, float FovY, float Aspect, float n, float f)
{
float w, h;

h = tanf (FovY / 2.0f);
w = h * Aspect;

MatDst.row [0] = Vector4 (1.0f / w, 0.0f, 0.0f, 0.0f);
MatDst.row [1] = Vector4 (0.0f, 1.0f / h, 0.0f, 0.0f);
MatDst.row [2] = Vector4 (0.0f, 0.0f, f / (f - n), 1.0f);
MatDst.row [3] = Vector4 (0.0f, 0.0f, n * f / (n - f), 0.0f);

return;
}

Wenn ich jetzt konstant Vector3 (0.0f, 1.0f, 0.0f) als Up an die Viewmatrix übergebe (was ja durchaus ein üblicher Upvector ist), dann habe ich beim ersten Kreuzprodukt schon 4 unnötige Multiplikationen mit 0 (die Multiplikationen mit 1 werden rausgekürzt, weil x*1 immer x ist, auch bei NaNs). Beim zweiten Kreuzprodukt kommen nochmal 2 dazu, weil AxisX.y ja definitiv 0 ist. Aus dem gleichen Grund ist auch je eine Multiplikation mit 0 bei der Normierung von AxisX und beim Skalarprodukt von AxisX und dem Eyevector überflüssig. Macht also insgesamt 8 unnötige Multiplikationen. Sicher, für diesen Fall könnte ich eine überladene Funktion mit explizit ausmultipliziertem 010-Upvector zusätzlich schreiben, aber spätestens bei der Verkettung von Matrizen hört sowas auf, da wird es dann nur noch unübersichtlich und dadurch fehleranfällig.

Eine normale perspektivische Projektionsmatrix (die zweite Funktion im Beispielcode) enthält 11 konstante Nullen, das ergibt normalerweise 44 unnötige Multiplikationen. Wenn man jetzt so eine mit einer frisch erzeugten Viewmatrix multipliziert, kennt der Compiler deren konstante Nullen noch, falls die Funktion inline expandiert wurde. Dadurch kann er schonmal 12 der Multiplikationen sparen, es kommen aber nochmal 3 dazu (konstante Nullen der Viewmatrix mit potentiellen NaNs der Projektionsmatrix). Die Erstellung und Verkettung dieser zwei Matrizen enthält also insgesamt 43 unnötige Multiplikationen. Wenn man die Viewmatrix mit dem impliziten 010-Upvector ausmultipliziert erzeugt, fallen wie gesagt schonmal 8 Multiplikationen weg. Eine konstante 0 entsteht noch zusätzlich in der Viewmatrix, dadurch fallen nochmal 3 Multiplikationen mit konstanten Nullen aus der Projektionsmatrix weg. Es bleiben also noch 32 Multiplikationen übrig, die nicht sein müßten. Und das ist der beste Fall für diese Verkettung, wenn man nicht die kombinierte Matrix per Hand erzeugen will. Das ginge zwar in diesem Fall und speziell dieses Beispiel ist auch eher unkritisch (wird in der Regel 1x pro Frame ausgeführt), aber es gibt noch deutlich mehr davon. Dazu kommen dann natürlich noch einige Additionen, die ebenfalls wegfallen würden, wenn x*0 konstant 0 ergeben würde.

Ich werde definitiv nichts explizit mit 0 multiplizieren, aber das Beispiel zeigt, wie es doch immer wieder implizit geschieht. Ich möchte aber nicht jede Stelle per Hand durchoptimieren müssen, sondern mich im Programmcode auch mal auf das wesentliche konzentrieren können. Ich mache mir so eine kleine Mathebibliothek ja eigentlich nur, um sie auch benutzen zu können. Wenn ich aber für die volle Performance an jeder Stelle manuell ran muß, dann wäre das ziemlich witzlos. Nicht nur, daß dann die Entwicklung viel länger dauert, sie wird auch unübersichtlicher und fehleranfälliger.

Trap[/POST]']0*inf gibt aber auch NaN, nur sagen dass kein NaN reinkommt macht es für den Compiler nicht besser.

Inf hatte ich nicht extra erwähnt, aber dafür gilt das natürlich auch. Ein "__assume (f * 0.0f == 0.0f)" hatte aber auch nichts gebracht.