Performance von Test gegen Null mit kleinem Epsilon für floats
-
Der "Trick" ist hier, dass eax das Register ist in dem der Return-Wert transportiert wird. Brauchst also eax nicht nochmal nach eax kopieren

-
das können wir sogar ohne inline-assembler machen:
inline bool zero(const float& a) { static const float eps = 0.0001f; return (reinterpret_cast<const unsigned&>(a)&(~0u>>1))<reinterpret_cast<const unsigned&>(eps); }der erzeugte code ist ggf. sogar noch effizienter. das ist mal ein seltener fall, wo es sich lohnt, builtins als referenz-auf-const zu übergeben, da die funktion eigentlich gar nicht mit dem argument in form eine floats arbeitet - jedenfalls verbleibt mit vc2005ee bei einfacher übergabe als float selbst bei höchster optimierung noch eine fld/fstp kombination, wenn das argument per value übergeben wird.
-
das "<" wird aber wahrscheinlich wieder in einen conditional-branch uebersetzt...
-
Also der VC++2005 macht aus camper's Beispiel (im Kontext einer if-Condition) folgendes.
float f; if (zero(f)) { }00401011 mov ecx,dword ptr [esp] 00401014 and ecx,7FFFFFFFh 0040101A cmp ecx,38D1B717h 00401020 jae main+31h (401031h)Jap, das ist effizienter

EDIT:
Feststellung: Ein gut optimierender Compiler hat schon was.
-
hellihjb schrieb:
das "<" wird aber wahrscheinlich wieder in einen conditional-branch uebersetzt...
zum glück gibt es seit ewigkeiten (PPro) die setcc befehle

-
Was passiert da?!?! Kann man den Code vom Compiler nicht einfach übernehmen? Ich versteh nur Bahnhof...
-
@FelixManke: Nein, in diesem Fall muss man dem optimierenden Compiler überlassen, aus camper's inline-Funktion und dem Zusammenhang "das Beste" zu machen...
00401011 mov ecx,dword ptr [esp] ; Kopiert float f (erste Var auf dem Stack, deshalb esp) nach ecx 00401014 and ecx,7FFFFFFFh ; s.o. 0040101A cmp ecx,38D1B717h ; Vergleich mit Konstante 00401020 jae main+31h (401031h) ; Sprung hinter das if (zero(f)) { ... } wenn nicht zeroEDIT:
Vorteil bei so einer Inline-Expansion ist, dass der Compiler noch weiter optimieren kann (was er bei Inline-Assemblercode nicht mehr kann), und sich nochdazu einen Sprung (in die Fkt. zero) spart.
-
ihr habt recht.
vc2003 macht so ziemich das gleiche draus:mov ecx,dword ptr [eax] and ecx,7FFFFFFFh cmp ecx,dword ptr [EPS (407030h)] sbb eax,eax neg eaxein hoch auf den compilerbauer!
jetzt waer' nur noch interessant, warum er das fabs() so verkackt

-
Der Code (die version mit dem reinterpret_cast und die entsprechende Assembler-Variante) verlässt sich ja ziemlich darauf, dass Floats so repräsentiert werden, wie sie anscheinend werden. Wann kann es da zu Problemen kommen? Nur bei anderen CPUs? Sind x86er immer so? Was ist mit 64 Bit Architekturen?
Ich muss das mal auf Geschwindigkeit testen. Aber wenn das nicht soo viel bringt, fühle ich mich mit C++ Code besser ;). Oder ich könnte nen #ifdef drumstricken und hätte noch ne Alternative im #else Zweig (z.B. die return (f >= -ZERO) && (f <=ZERO) Variante). Welche Präprozessor-Symbole brauche ich dann?
-
Was mir zum fabs() einfällt: Der Profiler braucht Debugcode. Da kann das alles natürlich etwas anders ausehen...
-
Der Code verlässt sich ja ziemlich darauf, dass Floats so repräsentiert werden, wie sie anscheinend werden. Wann kann es da zu Problemen kommen?
damit's nicht funktioniert muesste deine architektur keine 32bit-floats entsprechend IEEE-norm haben.
da kenn ich so spontan - keine.
irgendwer anders?
-
auf maschinen mit little-endian byte-order dürfte das sogar funktionieren, solange die representation der gleitkommazahl selbst IEEE konform ist und die größe dieser wenigstens so groß wie int ist. d.h. z.b. eine variante für double sollte auch funktionieren, ohne dass wir deswegen einen größeren integer benötigen (dabei geht nur etwas genauigkeit beim vergleich verloren, bei so einem epsilontest spielt das keine rolle).
-
Hab mal mit vs2002 Release kompilieren lassen (sorry für den häßlichen Output):
Meine IsZero1() { return fabs(f) <= CUtils::ZERO; }:
PUBLIC ?IsZero1@@YA_NM@Z ; IsZero1 ; Function compile flags: /Ogty ; COMDAT ?IsZero1@@YA_NM@Z _TEXT SEGMENT _f$ = 8 ?IsZero1@@YA_NM@Z PROC NEAR ; IsZero1, COMDAT ; 753 : return fabs(f) <= CUtils::ZERO; fld DWORD PTR _f$[esp-4] fabs fld DWORD PTR ?ZERO@CUtils@@2MB ; CUtils::ZERO fcompp fnstsw ax test ah, 1 jne SHORT $L123904 mov eax, 1 ; 754 : }; ret 0 $L123904: ; 753 : return fabs(f) <= CUtils::ZERO; xor eax, eax ; 754 : };IsZero2() { return (f >= -CUtils::ZERO) && (f <= CUtils::ZERO); }:
PUBLIC ?IsZero2@@YA_NM@Z ; IsZero2 ; Function compile flags: /Ogty ; COMDAT ?IsZero2@@YA_NM@Z _TEXT SEGMENT _f$ = 8 ?IsZero2@@YA_NM@Z PROC NEAR ; IsZero2, COMDAT ; 774 : return (f >= -CUtils::ZERO) && (f <= CUtils::ZERO); fld DWORD PTR ?ZERO@CUtils@@2MB ; CUtils::ZERO fchs fcomp DWORD PTR _f$[esp-4] fnstsw ax test ah, 65 ; 00000041H jp SHORT $L123910 fld DWORD PTR _f$[esp-4] fcomp DWORD PTR ?ZERO@CUtils@@2MB ; CUtils::ZERO fnstsw ax test ah, 65 ; 00000041H jp SHORT $L123910 mov eax, 1 ; 775 : }; ret 0 $L123910: ; 774 : return (f >= -CUtils::ZERO) && (f <= CUtils::ZERO); xor eax, eax ; 775 : };IsZero3() {return (reinterpret_cast<const unsigned&>(f)&(~0u>>1))<reinterpret_cast<const unsigned&>(CUtils::ZERO);}
PUBLIC ?IsZero3@@YA_NABM@Z ; IsZero3 ; Function compile flags: /Ogty ; COMDAT ?IsZero3@@YA_NABM@Z _TEXT SEGMENT _f$ = 8 ?IsZero3@@YA_NABM@Z PROC NEAR ; IsZero3, COMDAT ; 778 : return (reinterpret_cast<const unsigned&>(f)&(~0u>>1))<reinterpret_cast<const unsigned&>(CUtils::ZERO); mov eax, DWORD PTR _f$[esp-4] mov ecx, DWORD PTR [eax] mov eax, DWORD PTR ?ZERO@CUtils@@2MB and ecx, 2147483647 ; 7fffffffH cmp ecx, eax sbb eax, eax neg eax ; 779 : }
-
die optimierung, die reinterpret_cast<const unsigned&>(CUtils::ZERO) wie konstanten ausdruck behandelt, wird offenbar erst durch vc++8.0 durchgeführt.
behelfsmäßig könnte man ja soetwas machen:inline IsZero3() { assert(0x38D1B717h==reinterpret_cast<const unsigned&>(CUtils::ZERO)); return (reinterpret_cast<const unsigned&>(f)&(~0u>>1))<0x38D1B717h; }und so auch mit älteren compilern besseren code erhalten. die funktion sollte im übrigen unbedingt inline sein.
-
hellihjb schrieb:
damit's nicht funktioniert muesste deine architektur keine 32bit-floats entsprechend IEEE-norm haben.
da kenn ich so spontan - keine.
irgendwer anders?camper schrieb:
auf maschinen mit little-endian byte-order dürfte das sogar funktionieren, solange die representation der gleitkommazahl selbst IEEE konform ist und die größe dieser wenigstens so groß wie int ist. d.h. z.b. eine variante für double sollte auch funktionieren, ohne dass wir deswegen einen größeren integer benötigen (dabei geht nur etwas genauigkeit beim vergleich verloren, bei so einem epsilontest spielt das keine rolle).
Ich frag ja nur, weil der Code einfach funktionieren MUSS. Da darf nicht plötzlich Mist rauskommen, nur weil ich ein bisschen mit Assembler rumgespielt habe, und keiner merkt etwas :). So einen Fehler zu finden ist ja fast unmöglich.
Wie schaut's denn aus mit meinen Präprozessor-Symbolen? Gibt's welche?
-
Welche Zahl ist denn 0x38D1B717? Auf jeden Fall stammt die nicht mein CUtils::ZERO = 1e-5f
Aber wenn ich mir meinen Assemblercode anschaue, müsste ich wohl 2147483647 nehmen, oder?
-
der wert, der bei
cout << reinterpret_cast<const unsigned&>(CUtils::ZERO);ausgegeben wird gehört dort hin.
-
Hi,
also ich habe mich nun für die Variante
inline static bool IsZero(const float& f) { return (reinterpret_cast<const unsigned&>(f)&(~0u>>1))<reinterpret_cast<const unsigned&>(ZERO); };Ich wollte aber mal fragen, ob dies hier zu Geschwindigkeitseinbußen führt, oder ob das egal ist:
inline static bool IsZero(const float& f, float epsilon = ZERO) { return (reinterpret_cast<const unsigned&>(f)&(~0u>>1))<reinterpret_cast<const unsigned&>(epsilon); };Ich dachte, da das inline ist, sollte Compiler ja für den Wert von epsilon eine Konstante annehmen können und hätte dann dieselbe Ausgangssituation, wie im ersten Beispiel. Oder?
-
FelixManke schrieb:
Hi,
also ich habe mich nun für die Variante
inline static bool IsZero(const float& f) { return (reinterpret_cast<const unsigned&>(f)&(~0u>>1))<reinterpret_cast<const unsigned&>(ZERO); };Ich wollte aber mal fragen, ob dies hier zu Geschwindigkeitseinbußen führt, oder ob das egal ist:
inline static bool IsZero(const float& f, float epsilon = ZERO) { return (reinterpret_cast<const unsigned&>(f)&(~0u>>1))<reinterpret_cast<const unsigned&>(epsilon); };Ich dachte, da das inline ist, sollte Compiler ja für den Wert von epsilon eine Konstante annehmen können und hätte dann dieselbe Ausgangssituation, wie im ersten Beispiel. Oder?
theoretisch - sicher. die frage ist ja, was in der praxis passiert. bei meinen versuchen mit vc++8.0 stellt sich heraus, das dieser compiler nur dann optimal optimiert (d.h. der int-wert von epsilon ist unmittelbar teil des op-codes), wenn epsilon static storage duration hat und durch einen konstanten ausdruck (d.h. hier ein literal) initialisiert wird. schon das weglassen des static in meinem ursprünglichen code verhindert diese optimierung - eine übergabe als funktionsparameter dürfte kein besseres ergebnis bringen. letzlich musst du dir einfach mal den assmblercode anschauen bzw. messungen durchführen, um den besten kompromiss zwischen ausführungsgeschwindigkeit und flexibilität herauszufinden.
-
hellihjb schrieb:
bzgl gcc gibt's irgendwelche flags die auch inline-assembler mit intel-syntax erlauben, hab's aber auch nicht mehr im kopf - muesste sich per suche finden lassen.
Das würde mich unglaublich interessieren. Falls es irgendjemand anderem einfällt, wäre ich sehr dankbar darüber
