Forum: Compiler & IDEs Test von Gleitkommazahl auf 0 führt zum Aufruf von __cmpsf2


von Flockinger (Gast)


Lesenswert?

Um bei der Berechnung von schwach besetzten Matrizen Zeit zu sparen 
(floating point Matrizen auf einem ATmega64 sind ja eh schon 
sadistisch), mache ich vor jeder Multiplikation einen schnellen 
Vergleich, ob einer der beiden Faktoren 0 ist, um in diesem Fall statt 
der Multiplikation einfach eine Zuweisung von 0.0 an die 
Ergebnisvariable zu machen.

Beim betrachten der Compilerausgabe (.lss) ist mir allerdings 
aufgefallen, dass dieser Vergleich längst nicht so schnell ist wie 
gedacht, denn statt die 4 Bytes eines jeden Werts einfach auf 0 zu 
testen und dann zu verzweigen, wird jeweils 0x00000000 in vier Register 
geladen und anschließend __cmpsf2 aufgerufen.

Hier eine der betreffenden Stellen:
1
if(adiskm.P[elmt][row] && adiskm.H[col][elmt]) adiskm.B[row][col] += adiskm.P[elmt][row] * adiskm.H[col][elmt];

und was daraus (aus dem Vergleich) in der .lss wird:
1
    9c4e:  ld  r14, Y
2
    9c50:  ldd  r15, Y+1  ; 0x01
3
    9c52:  ldd  r16, Y+2  ; 0x02
4
    9c54:  ldd  r17, Y+3  ; 0x03
5
    9c56:  subi  r28, 0xAD  ; 173
6
    9c58:  sbci  r29, 0x01  ; 1
7
    9c5a:  movw  r24, r16
8
    9c5c:  movw  r22, r14
9
    9c5e:  ldi  r18, 0x00  ; 0
10
    9c60:  ldi  r19, 0x00  ; 0
11
    9c62:  ldi  r20, 0x00  ; 0
12
    9c64:  ldi  r21, 0x00  ; 0
13
    9c66:  call  0xca94  ; 0xca94 <__cmpsf2>

Wie kann man dem Compiler klar machen, dass er in diesem speziellen Fall 
bitte die deutlich einfachere und schnellere Variante nehmen soll?
Mir fällt nur die Möglichkeit ein, eine eigene Funktion dafür zu 
schreiben, da ein typecast auf einen int Typ ja wieder eine 
Konvertierungsfunktion aufrufen würde.

GCC Version ist 4.5.3 unter Kubuntu mit Kernel 2.6.32-34, Optimierung 
steht auf Level 3.

von (prx) A. K. (prx)


Lesenswert?

Flockinger schrieb:

> Wie kann man dem Compiler klar machen, dass er in diesem speziellen Fall
> bitte die deutlich einfachere und schnellere Variante nehmen soll?

Compiler umschreiben. ;-)

> schreiben, da ein typecast auf einen int Typ ja wieder eine
> Konvertierungsfunktion aufrufen würde.

Nicht bei sowas wie
1
#define FloatIsNull(x) (*(int32_t*)&(x) == 0)

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

A. K. schrieb:
> Flockinger schrieb:
>
>> Wie kann man dem Compiler klar machen, dass er in diesem speziellen Fall
>> bitte die deutlich einfachere und schnellere Variante nehmen soll?
>
> Compiler umschreiben. ;-)
>
>> schreiben, da ein typecast auf einen int Typ ja wieder eine
>> Konvertierungsfunktion aufrufen würde.
>
> Nicht bei sowas wie
1
> #define FloatIsNull(x) (*(int32_t*)&(x) == 0)

• Erstens macht es nicht das, was es soll denn die
  Darstellung der 0 ist nicht eindeutig.
• Und Zweitens kollidiert das mit den Aliasing-Regeln:
  long ist kein Alias für float.

von (prx) A. K. (prx)


Lesenswert?

Johann L. schrieb:

> • Erstens macht es nicht das, was es soll denn die
>   Darstellung der 0 ist nicht eindeutig.

Da es hier nur zur Beschleunigung von teilweise mit 0 besetzten Matrizen 
vorgesehen war ist das weniger relevant.

Allgemein betrachtet sind die Operatoren == und != bei Fliesskommadaten 
ohnehin problematisch.

> • Und Zweitens kollidiert das mit den Aliasing-Regeln:
>   long ist kein Alias für float.

Stimmt.

von Flockinger (Gast)


Lesenswert?

Vielen Dank für eure Tipps. Leider führt die Variante über Pointer 
casten dazu, dass die Werte - obwohl gerade erst in Register geladen - 
erneut aus dem SRAM geladen werden, was wieder 8 ClkCycles zusätzlich 
braucht. Das ist zwar gegenüber der __mulsf3 kaum was (2*8/138=11,6%), 
aber dennoch wollte ich nicht einsehen, kostbare Rechenzeit zu 
verschwenden.

Daher habe ich doch eine Funktion geschrieben, allerdings als inline 
Funktion mit inline assembler:
1
inline uint8_t FloatIsNull(float value_to_test) {
2
  uint8_t testresult;
3
  asm ("  BST %D1,7          ;store sign bit for restoration      ""\n\t"
4
    "  CBR %D1,0x80        ;clear sign bit to detect 0.0 and -0.0  ""\n\t"
5
    "  SUBI %A1,0          ;test low byte              ""\n\t"
6
    "  SBCI %B1,0          ;                    ""\n\t"
7
    "  SBCI %C1,0          ;                    ""\n\t"
8
    "  SBCI %D1,0          ;Flag Z is set if all bytes were 0x00  ""\n\t"
9
    "  LDI %0,1          ;                    ""\n\t"
10
    "  BREQ 1f            ;Skip if Flag Z is set          ""\n\t"
11
    "  CLR %0            ;                    ""\n\t"
12
    "1:                                    ""\n\t"
13
    "  BLD %D1,7          ;restore sign bit            ""\n\t"
14
    : [retval] "=d" (testresult)
15
    : [testval] "d" (value_to_test)  );
16
  return testresult;
17
}

alternative:
1
inline uint8_t FloatIsNull(float value_to_test) {
2
  uint8_t testresult;
3
  asm ("  BST %D1,7          ;store sign bit for restoration      ""\n\t"
4
    "  LSL %D1            ;clear sign bit to detect 0.0 and -0.0  ""\n\t"
5
    "  CP %A1,__zero_reg__      ;test low byte              ""\n\t"
6
    "  CPC %B1,__zero_reg__    ;                    ""\n\t"
7
    "  CPC %C1,__zero_reg__    ;                    ""\n\t"
8
    "  CPC %D1,__zero_reg__    ;Flag Z is set if all bytes were 0x00  ""\n\t"
9
    "  LDI %0,1          ;                    ""\n\t"
10
    "  BREQ 1f            ;Skip if Flag Z is set          ""\n\t"
11
    "  CLR %0            ;                    ""\n\t"
12
    "  LSR %D1            ;restore highest byte if not Null    ""\n\t"
13
    "1:                                    ""\n\t"
14
    "  BLD %D1,7          ;restore sign bit            ""\n\t"
15
    : [retval] "=d" (testresult)
16
    : [testval] "r" (value_to_test)  );
17
  return testresult;
18
}

Die obere Variante braucht ein Wort und einen Takt weniger, dafür müssen 
sowohl Input als auch Output im oberen Registerbereich liegen. Die 
zweite Variante erlaubt zu genau diesem Preis die allokation der float 
Variable im gesamten Registerbereich.

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Das ist nicht korrekt:

%0 ist earl-clobber, die Constraint muss also "=&d" sein anstatt "=d".

Beispielsweise wird es falsch, wenn der Compiler value_to_test nach R16 
leht und value_to_test nach R17. Dies ist möglich, falls value_to_test 
nach dem test nicht mehr verwendet wird.

Denkst du, eine entsprechende Optimierung im Compiler wäre sinnvoll?

Was sind die Voraussetzungen dafür?

-funsafe-math-optimizations?
-ffinite-math-only?
-fno-signaling-nans?

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Bei dem Vergleich kommt man auch ohne early-clobber aus:
1
;; %1 == 0f -> %0 = 0
2
;; %1 != 0f -> %0 = -1
3
mov __tmp_reg__,%D1
4
lsl %D1,__tmp_reg__
5
cp  __zero_reg__,%A1
6
cpc __zero_reg__,%B1
7
cpc __zero_reg__,%C1
8
cpc __zero_reg__,%D1
9
mov %D1,__tmp_reg__
10
sbc %0,%0

von Flockinger (Gast)


Lesenswert?

Hallo Johann,

nach einigem überlegen hab ich herausgefunden, was du meinst (lies dir 
bitte dein Posting nochmal durch und versuche zu erkennen, dass solche 
Tippfehler ein Verständnis deines Postings erheblich erschweren).

Dein Argument ist also, dass die Wiederherstellung von %D1 zu einem 
falschen Rückgabewert führen kann, wenn %0 und %D1 im selben Register 
abgelegt werden. Da muss ich dir Recht geben. Mir schien es in dem 
Moment wichtiger, dass die Gleitkommazahl auf jeden Fall erhalten 
bleibt. Deiner Argumentation nach ist der Compiler aber wohl schlau 
genug zu wissen, dass er das Ausgangsregister nicht auf den Eingang 
legt, wenn er den Eingang danach noch benötigt.

Deine Variante das Carry-Bit zu benutzen ist gut, so kommt man um den 
Branch rum, der bei Verwendung des Zero-Bits unvermeidbar ist. 
Allerdings wird dabei - wie du in deinem Kommentar bereits angedeutet 
hast - die Funktion invertiert, weil das Carry-Bit ja 1 wird, wenn die 
Zahl ungleich 0 ist. Außerdem nimmt LSL nur ein Argument, das 
_tmp_reg_ kannst du dir also sparen. Die gesamte Funktion würde dann 
so aussehen und benötigt unabhängig vom Eingang immer 8 ClockCycles:
1
inline uint8_t FloatIsNotNull(float value_to_test) {
2
  uint8_t testresult;
3
  asm ("MOV __tmp_reg__,%D1   ;store sign bit for restoration""\n\t"
4
      "LSL %D1                ;clear sign bit to detect 0.0 and -0.0""\n\t"
5
      "CP __zero_reg__,%A1    ;test low byte              ""\n\t"
6
      "CPC __zero_reg__,%B1   ;                    ""\n\t"
7
      "CPC __zero_reg__,%C1   ;                    ""\n\t"
8
      "CPC __zero_reg__,%D1   ;Flag C is set if any byte was >0x00""\n\t"
9
      "MOV %D1,__tmp_reg__    ;restore sign bit            ""\n\t"
10
      "SBC %0,%0              ;-1 if Carry was set, 0 if not""\n\t"
11
    : [retval] "=d" (testresult)
12
    : [testval] "d" (value_to_test)  );
13
  return testresult;
14
}

Was deine Fragen bezüglich Compiler-Optimierung angeht, verstehe ich 
deine Vorwürfe nicht. Die Funktion tut genau das, was ihr Name aussagt. 
Dass z.B. eine Multiplikation 0*inf ein NaN ergibt und daher ausgeführt 
werden sollte, obwohl einer der beiden Operanden 0 ist, liegt in der 
Verantwortung des Benutzers. Hier kann man ja die bereits existierenden 
Funktionen isnan() und isinf() heranziehen. Diese Fälle hatte ich bei 
mir zwar nicht abgedeckt, aber das geht an der Aufgabe des Compilers 
vorbei. Ich hatte eine Abfrage gestellt, ob der Wert 0 ist und genau das 
war mir nicht schnell genug.

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Flockinger schrieb:

> Dein Argument ist also, dass die Wiederherstellung von %D1 zu einem
> falschen Rückgabewert führen kann, wenn %0 und %D1 im selben Register
> abgelegt werden. Da muss ich dir Recht geben. Mir schien es in dem
> Moment wichtiger, dass die Gleitkommazahl auf jeden Fall erhalten
> bleibt.

Beides ist wichtig. Unterschied ist nur, daß ein Fehler seltener 
auftritt als der andere und nicht so leicht zu finden ist.

> Deiner Argumentation nach ist der Compiler aber wohl schlau
> genug zu wissen, dass er das Ausgangsregister nicht auf den Eingang
> legt, wenn er den Eingang danach noch benötigt.

Genau.

Wenn das Ausgangsregister sich allerdingsmit dem Eingangsregister 
überlappt, und das E-Register verwendet wird, nachdem das A-Register 
verändert wurde, gibt es ein Problem ohne early-clobber.

> Deine Variante das Carry-Bit zu benutzen ist gut, so kommt man um den
> Branch rum, der bei Verwendung des Zero-Bits unvermeidbar ist.
> Allerdings wird dabei - wie du in deinem Kommentar bereits angedeutet
> hast - die Funktion invertiert, weil das Carry-Bit ja 1 wird, wenn die
> Zahl ungleich 0 ist.

Ja, der Schnippel liefert quasi
1
wert != 0 ? -1 : 0;

> Was deine Fragen bezüglich Compiler-Optimierung angeht, verstehe ich
> deine Vorwürfe nicht.

Vorwürfe??

> Dass z.B. eine Multiplikation 0*inf ein NaN ergibt und daher ausgeführt
> werden sollte, obwohl einer der beiden Operanden 0 ist, liegt in der
> Verantwortung des Benutzers. Hier kann man ja die bereits existierenden
> Funktionen isnan() und isinf() heranziehen. Diese Fälle hatte ich bei
> mir zwar nicht abgedeckt, aber das geht an der Aufgabe des Compilers
> vorbei.

Auch Vergleiche können gegen eine INF oder NaN gehen.

> Ich hatte eine Abfrage gestellt, ob der Wert 0 ist und genau das
> war mir nicht schnell genug.

Meine Frage war, unter welchen Bedingungen es legitim ist, eine solche 
Optimierung bereits im Compiler zu machen. Der Code ist ja recht einfach 
und zudem weiß der Compiler, ob er die Eingabe zerstören darf weil die 
nicht mehr benötogt wird, oder eben nicht.

Momentan ist es so, das der AVR-Teil im GCC keine float-Vergleiche 
unterstützt, und diese Vergleiche einfach auf Aufrufe von 
Bibliotheksfunktionen wie __eqsf2 für a == b abgebildet werde.

Fasst man diese Vergleiche im AVR-Teil an und sagt "Ja, avr-gcc kann 
Floats gegeneinander vergleichen", dann ist es nicht möglich, sich 
spezielle Vergleiche rauszupicken. Stattdessen muss man alle 
float-Vergleiche implementieren — zumindest alle ordered Comparisons 
gegen alle Werte.

von Flockinger (Gast)


Lesenswert?

Johann L. schrieb:
> Vorwürfe??

Deine Optionen klangen so, als würdest du mir vorwerfen, unsauberen Code 
zu schreiben, der z.B. bei +-INF oder NaN nicht funktioniert.
Sorry, falls ich da etwas zu negativ in der Interpretation war.

> Auch Vergleiche können gegen eine INF oder NaN gehen.

Das ist richtig, aber mir gings ja wie gesagt nur um den Spezialfall 
==0.0 und das funktioniert (zumindest die hier gezeigte Variante) auch 
gegen INF und NaN.

> Meine Frage war, unter welchen Bedingungen es legitim ist, eine solche
> Optimierung bereits im Compiler zu machen. Der Code ist ja recht einfach
> und zudem weiß der Compiler, ob er die Eingabe zerstören darf weil die
> nicht mehr benötogt wird, oder eben nicht.
>
> Momentan ist es so, das der AVR-Teil im GCC keine float-Vergleiche
> unterstützt, und diese Vergleiche einfach auf Aufrufe von
> Bibliotheksfunktionen wie __eqsf2 für a == b abgebildet werde.

OK, das ist die Erklärung, warum der Compiler einfach alles auf die 
__cmpsf2 biegt.

> Fasst man diese Vergleiche im AVR-Teil an und sagt "Ja, avr-gcc kann
> Floats gegeneinander vergleichen", dann ist es nicht möglich, sich
> spezielle Vergleiche rauszupicken. Stattdessen muss man alle
> float-Vergleiche implementieren — zumindest alle ordered Comparisons
> gegen alle Werte.

Du meinst also, es ist nicht möglich eine Abfrage in den Compiler 
einzubauen, die spezielle Vergleiche (wie z.B. auf ==0.0 oder !=0.0) 
durch Kurzlösungen ersetzt und nur den Rest an die allgemeine 
Bibliotheksfunktion weiter reicht?
Vielleicht sollte man dann die obige Funktion als solche in die avr-libc 
mitaufnehmen, analog zu isnan() oder isinf() um auch Benutzern, die kein 
Assembler können und diesen Thread nicht finden, die Möglichkeit zu 
geben, einen schnellen Vergleich von floats auf ==0.0 in ihren 
Programmen zu verwenden.

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Flockinger schrieb:
> Johann L. schrieb:
>> Fasst man diese Vergleiche im AVR-Teil an und sagt "Ja, avr-gcc kann
>> Floats gegeneinander vergleichen", dann ist es nicht möglich, sich
>> spezielle Vergleiche rauszupicken. Stattdessen muss man alle
>> float-Vergleiche implementieren — zumindest alle ordered Comparisons
>> gegen alle Werte.
>
> Du meinst also, es ist nicht möglich eine Abfrage in den Compiler
> einzubauen, die spezielle Vergleiche (wie z.B. auf ==0.0 oder !=0.0)
> durch Kurzlösungen ersetzt und nur den Rest an die allgemeine
> Bibliotheksfunktion weiter reicht?

Möglich ist es.  GCC ist ja kein Hexenwerk sondern von Menschenhand.
Aber es ist nicht einfach im AVR-Teil zu machen.
Dokumentiert ist es in can_compare_p in optabs.c:
1
   Nonzero if we can perform a comparison of mode MODE straightforwardly.
2
   PURPOSE describes how this comparison will be used.  CODE is the rtx
3
   comparison code we will be using.
4
5
   ??? Actually, CODE is slightly weaker than that.  A target is still
6
   required to implement all of the normal bcc operations, but not
7
   required to implement all (or any) of the unordered bcc operations.  */
http://gcc.gnu.org/viewcvs/trunk/gcc/optabs.c?content-type=text%2Fplain&view=co

Um es einfach zu machen, müsste erst diese Restruktion beseitigt werden.
Aber ähnlich Restriktonen gibt es für viele andere Pattern auch: 
Entweder alles oder nix, Rosinen rauspicken geht nur bei wenigen Pattern 
wie zB Rotate.

Leider ist es auch nicht möglich, einen Combine-Pattern zu schreiben, 
weil ds erzeugte Pattern einen Fuktionsaufruf darstellt.

> Vielleicht sollte man dann die obige Funktion als solche in die avr-libc
> mitaufnehmen, analog zu isnan() oder isinf() um auch Benutzern, die kein
> Assembler können und diesen Thread nicht finden, die Möglichkeit zu
> geben, einen schnellen Vergleich von floats auf ==0.0 in ihren
> Programmen zu verwenden.

Die avr-libc kann bereits float-Vergleiche, zumindest hab ich da ein 
__gesf2 gesehen. Was genau unterstützt wird weiß ich aber nicht; 
ebensowenig, ob sich diese Funktionen die Operanden explizit anschauen 
und abkürzen, falls mindestens einer 0.0 ist.

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.