2016-01-28 18 views
6

Ich bin ein Projekt zu tun, wo ich RGB zu Luma Umwandlungen tun, und ich habe einige Runden Probleme mit der -mno-sse2 Flagge:gcc -mno-sse2 Runde

Hier ist der Testcode:

#include <stdio.h> 
#include <stdint.h> 

static double rec709_luma_coeff[3] = {0.2126, 0.7152, 0.0722}; 

int main() 
{ 
    uint16_t n = 242 * rec709_luma_coeff[0] + 242 * rec709_luma_coeff[1] + 242 * rec709_luma_coeff[2]; 

    printf("%u\n", n); 
    return 0; 
} 

Und hier ist, was ich bekommen:

[email protected]>gcc -mno-sse2 test.c -o test && ./test 
241 
[email protected]> gcc test.c -o test && ./test 
242 

ich nehme an, dass gcc sse2 Optimierungen für double Multiplikationen verwendet, aber was ich nicht bekommen, ist, warum die optimierte Version die richtige wäre.

Auch, was empfehle ich, um konsistentere Ergebnisse zu erhalten, ceil() oder floor()?

+5

Es hat nichts mit Optimierung zu tun. Nein SSE2 bedeutet die Verwendung von alten x87 FPU, die ** breiter ** als SSE2 ist. In gewissem Sinne werden x87-Ergebnisse mit höherer Genauigkeit ausgeführt, aber die Ergebnisse können sich von denen unter SSE2 unterscheiden. –

+0

Seltsamerweise verschwindet das Problem bei der Kompilierung mit dem Flag "-O2" ... – perror

+0

Wenn Sie die Optimierung aktivieren, erhalten Sie auch 242 mit '-mno-sse2' – jofel

Antwort

0

TL: DR verwenden lrint(x) oder (int)rint(x), um von Float zu Int mit Rundung auf nächste statt Kürzung zu konvertieren. Leider arbeiten nicht alle Compiler effizient mit den gleichen mathematischen Funktionen. Siehe round() for float in C++


gcc -mno-sse2 hat x87 für double, auch in 64-Bit-Code zu verwenden. x87-Register haben eine interne Genauigkeit von 80 Bit, aber SSE2 verwendet das Format IEEE binary64 (aka double) nativ in XMM-Registern, so dass alle Provisorien in jedem Schritt auf 64-Bit double gerundet werden.

Das Problem ist nicht so interessant wie the double rounding problem (80 Bit -> 64 Bit, dann zu Integer). Es ist auch nicht von gcc -O0 (der Standard: keine zusätzlichen Optimierungen) Runden beim Speichern von Provisorien im Speicher, weil Sie das Ganze in einer C-Anweisung gemacht haben, so dass es nur x87 Register für den gesamten Ausdruck verwendet.


Es ist einfach, dass 80-Bit-Genauigkeit zu einem Ergebnis führt, die knapp unterhalb 242,0 und wird abgeschnitten, bis 241 durch C die Schwimmer-> int Semantik, während SSE2 ein Ergebnis gerade oberhalb 242,0 welche auf 242 abschneidet erzeugt. Für x87 erfolgt das Abrunden auf die nächstniedrigere Ganzzahl konsistent, nicht nur 242, für eine Eingabe von 1 bis 65535. (Ich habe eine Version Ihres Programms mit atoi(argv[1]) erstellt, so dass ich andere Werte testen konnte, und mit -O3).

Denken Sie daran, dass int foo = 123.99999 123 ist, da C den Abrundungsmodus "truncation" (Richtung Null) verwendet. Für nicht-negative Zahlen ist dies das gleiche wie floor (das auf -Infinity aufrundet). https://en.wikipedia.org/wiki/Floating-point_arithmetic#Rounding_modes.


double können die Koeffizienten repräsentieren genau: Ich druckte sie mit gdb und bekam: {0.21260000000000001, 0.71519999999999995, 0.0722}. Diese Dezimaldarstellungen sind wahrscheinlich keine exakten Darstellungen der Basis-2-Gleitkommawerte. Aber sie sind nahe genug, um zu sehen, dass sich die Koeffizienten zu 0.99999999999999996 addieren (mit einem Rechner mit beliebiger Genauigkeit).

Wir bekommen bestehen im Abrunden weil x87 interne Genauigkeit höher als die Genauigkeit der Koeffizienten ist, so dass die Summe von Fehlern in n * rec709_luma_coeff[0] Runden und so weiter, und die Ergebnisse zusammenfassend, sind ~ 2^11 kleiner ist als die Differenz zwischen der Summe der Koeffizienten und 1,0. (64-Bit-Signifikanz gegenüber 53 Bits).

Die eigentliche Frage ist, wie die SSE2-Version funktionierte! Vermutlich Runde auf nächste - auch auf den Provisorien passiert in genügend Fällen nach oben, zumindest für 242. Es produziert zufällig die ursprüngliche Eingabe für mehr Fälle als nicht, aber es erzeugt Input-1 für 5, 7, 10, 13, 14, 20 ... (252 der ersten 1000 Zahlen von 1..1000 sind "munged" durch die SSE2-Version, wird es so nicht, wie es funktioniert entweder immer.)


Mit -O3 für Ihre Quelle, Es führt die Berechnung zur Kompilierzeit mit erweiterter Genauigkeit durch und erzeugt das genaue Ergebnis. d.h. er kompiliert das gleiche wie printf("%u\n", n);.


Und BTW sollten Sie staticconst für Sie Konstanten verwenden, so gcc besser optimieren können. static ist jedoch viel besser als plain global, weil der Compiler sehen kann, dass nichts in der Kompilierungseinheit die Werte schreibt oder ihre Adresse irgendwo gibt, so dass es sie behandeln kann, als wären sie const.