Der schnellste Weg, um einen realen (Fest- / Gleitkomma-) Wert zu klemmen?

Lesezeit: 13 Minuten

Benutzeravatar von Niklas
Niklas

Gibt es eine effizientere Möglichkeit, reelle Zahlen zu klemmen, als if-Anweisungen oder ternäre Operatoren zu verwenden? Ich möchte dies sowohl für Doubles als auch für eine 32-Bit-Fixpoint-Implementierung (16.16) tun. Ich bin nicht nach Code fragen, der beide Fälle handhaben kann; Sie werden in separaten Funktionen behandelt.

Natürlich kann ich so etwas tun:

double clampedA;
double a = calculate();
clampedA = a > MY_MAX ? MY_MAX : a;
clampedA = a < MY_MIN ? MY_MIN : a;

oder

double a = calculate();
double clampedA = a;
if(clampedA > MY_MAX)
    clampedA = MY_MAX;
else if(clampedA < MY_MIN)
    clampedA = MY_MIN;

Die Fixpoint-Version würde Funktionen/Makros für Vergleiche verwenden.

Dies geschieht in einem leistungskritischen Teil des Codes, daher suche ich nach einem möglichst effizienten Weg, dies zu tun (was meiner Meinung nach eine Bitmanipulation beinhalten würde).

EDIT: Es muss Standard/Portable C sein, plattformspezifische Funktionalität ist hier nicht von Interesse. Ebenfalls, MY_MIN und MY_MAX sind vom gleichen Typ wie der Wert, den ich festklemmen möchte (in den obigen Beispielen verdoppelt).

  • Ich denke, Sie könnten dafür SSE3 oder eine ähnliche Technologie verwenden, wissen aber nicht genau, welche Befehle / wie … Sie können sich Folgendes ansehen: Arithmetik der Sättigung

    – rkj

    9. Januar 2009 um 9:25 Uhr

  • Entschuldigung, die Frage zu den Plattformanforderungen war nicht klar. Ich habe die Frage bearbeitet, um sie etwas zu klären.

    – Niklas

    9. Januar 2009 um 9:38 Uhr

  • Ich weiß, dass es zweieinhalb Jahre her ist, seit Sie diese Frage gestellt haben, aber ich hoffe, Sie überprüfen meine Antwort – eine 3-fache Verbesserung ist signifikant.

    – Markieren Sie Lösegeld

    16. September 2011 um 3:31 Uhr

  • Ein nicht spezifiziertes Detail ist, welche Genauigkeit (relativ oder absolut) Sie bereit sind, gegen Geschwindigkeit einzutauschen – falls vorhanden. Wenn der Code einen In-Range-Wert erfordert a genau wie zurückgegeben werden a, dann erfüllen viele Antworten diese Hürde nicht. Wenn es auf Präzision ankommt nein Sorge , dann immer wieder (MY_MAX + MY_MIN)/2 wird sicherlich eine schnelle Antwort mit geringer Genauigkeit und sicherlich dumm sein. Empfehlen Sie, nicht mehr als 1 zu tolerieren ULP Error.

    – chux – Wiedereinsetzung von Monica

    8. Januar 2016 um 19:24 Uhr

  • Wie würden Sie es mit der SSE4-Variablen machen (__m128)?

    – Royi

    11. Februar 2018 um 10:16 Uhr

Benutzeravatar von Jorge
Jörg

Sowohl GCC als auch Clang generieren eine schöne Assemblierung für den folgenden einfachen, unkomplizierten, portablen Code:

double clamp(double d, double min, double max) {
  const double t = d < min ? min : d;
  return t > max ? max : t;
}

> gcc -O3 -march=native -Wall -Wextra -Wc++-compat -S -fverbose-asm clamp_ternary_operator.c

GCC-generierte Assembly:

maxsd   %xmm0, %xmm1    # d, min
movapd  %xmm2, %xmm0    # max, max
minsd   %xmm1, %xmm0    # min, max
ret

> clang -O3 -march=native -Wall -Wextra -Wc++-compat -S -fverbose-asm clamp_ternary_operator.c

Clang-generierte Assembly:

maxsd   %xmm0, %xmm1
minsd   %xmm1, %xmm2
movaps  %xmm2, %xmm0
ret

Drei Anweisungen (Ret nicht mitgezählt), keine Verzweigungen. Exzellent.

Dies wurde mit GCC 4.7 und Clang 3.2 auf Ubuntu 13.04 mit einem Core i3 M 350 getestet. Nebenbei bemerkt, der einfache C++-Code, der std::min und std::max aufruft, generierte dieselbe Assembly.

Dies ist für Doppel. Und für int erzeugen sowohl GCC als auch Clang eine Assembly mit fünf Anweisungen (ohne ret) und ohne Verzweigungen. Auch ausgezeichnet.

Ich verwende derzeit kein Festkomma, daher werde ich keine Meinung zu Festkomma abgeben.

  • Groß. Etwas besser als eine gute Antwort, da es symmetrisch gehandhabt wird min und/oder max wenn einer/beide Nicht-A-Nummern sind. Es bewahrt auch Zeichen mit d = -0.0!

    – chux – Wiedereinsetzung von Monica

    8. Januar 2016 um 18:50 Uhr

  • Verwenden if (d < min) und if (d > max) gibt mir auch den gleichen Assembler-Code. Es ist jedoch interessant zu sehen, dass using if (d < min) und else if (d > max) erzeugt eine andere Ausgabe (es gibt eine Sprunganweisung).

    – elboulangero

    21. Juni 2017 um 0:45 Uhr

  • Genau. Dies sollte die richtige Antwort sein. Dies ist eine Compiler-Analyse für die Frage: godbolt.org/z/ZW4W6F

    – Felipe Lavratti

    21. Oktober 2018 um 12:33 Uhr


  • Getestet mit MSVC 2019, kompiliert auch zu keinen Branches (zumindest für Floats).

    – Gabriele Giuseppini

    2. Februar 2020 um 22:50 Uhr

Spats Benutzeravatar
Spucke

Alte Frage, aber ich habe heute an diesem Problem gearbeitet (mit Doubles/Floats).

Der beste Ansatz ist die Verwendung von SSE MINSS/MAXSS für Floats und SSE2 MINSD/MAXSD für Doubles. Diese sind verzweigungslos und benötigen jeweils einen Taktzyklus und sind dank Compiler-Intrinsic einfach zu verwenden. Sie verleihen eine Leistungssteigerung von mehr als einer Größenordnung im Vergleich zu einer Klemmung mit std::min/max.

Das mag Sie überraschen. Das habe ich auf jeden Fall! Leider verwendet VC++ 2010 einfache Vergleiche für std::min/max, selbst wenn /arch:SSE2 und /FP:fast aktiviert sind. Ich kann nicht für andere Compiler sprechen.

Hier ist der notwendige Code, um dies in VC++ zu tun:

#include <mmintrin.h>

float minss ( float a, float b )
{
    // Branchless SSE min.
    _mm_store_ss( &a, _mm_min_ss(_mm_set_ss(a),_mm_set_ss(b)) );
    return a;
}

float maxss ( float a, float b )
{
    // Branchless SSE max.
    _mm_store_ss( &a, _mm_max_ss(_mm_set_ss(a),_mm_set_ss(b)) );
    return a;
}

float clamp ( float val, float minval, float maxval )
{
    // Branchless SSE clamp.
    // return minss( maxss(val,minval), maxval );

    _mm_store_ss( &val, _mm_min_ss( _mm_max_ss(_mm_set_ss(val),_mm_set_ss(minval)), _mm_set_ss(maxval) ) );
    return val;
}

Der Code mit doppelter Genauigkeit ist derselbe, außer dass stattdessen xxx_sd verwendet wird.

Bearbeiten: Anfangs habe ich die Klemmfunktion wie kommentiert geschrieben. Aber als ich mir die Assembler-Ausgabe ansah, bemerkte ich, dass der VC++-Compiler nicht schlau genug war, um die redundante Bewegung auszumerzen. Eine Anweisung weniger. 🙂

  • Gibt es ein Äquivalent für diese Funktionen für GCC?

    – Dan

    25. März 2013 um 19:28 Uhr

  • Ja, für GCC x86-Nutzung __builtin_ia32_storess, __builtin_ia32_maxss__builtin_ia32_minss` sind die äquivalenten Funktionen und die xmmintrin.h Header für SSE1-Anweisungen. Passieren -mmmx -msse an den Compiler, die Sie möglicherweise benötigen -mfpmath=sse(,x87) auch. Intrinsics sind auch für ARM Neon und AltiVec verfügbar. Sehen X86 Eingebaute Funktionen für mehr Details.

    – mctylr

    28. Mai 2014 um 14:13 Uhr


  • Es ist nicht möglich, dass der Compiler ersetzt std::min und std::max mit den Intrinsics im allgemeinen Fall, da die Intrinsics das IEEE754-spezifizierte Ergebnis liefern min(2.0, NaN) und min(NaN, 2.0) (welches ist 2.0 in beiden Fällen), während eine naive Implementierung, die auf einem einzelnen Vergleich basiert, ein inkonsistentes Ergebnis basierend auf der Parameterreihenfolge zurückgibt. C99 und C++11 bieten fmax und fminund ein cleverer Compiler ersetzt diese durch effiziente Inline-Implementierungen.

    – Strkat

    1. Juni 2014 um 4:41 Uhr


  • Gibt es eine Umschaltstrafe für die Verwendung von SSE-Anweisungen oder deren Verschachtelung mit Standard-Gleitkommaoperationen?

    – Robinson

    6. Mai 2015 um 11:36 Uhr

  • Das scheint wirklich hilfreich zu sein — kennt jemand irgendwo eine vollständige Implementierung, zB mit richtigen #ifdef’s für gcc und clang etc.?

    – Bob

    12. Juli 2016 um 16:42 Uhr

Wenn Ihr Prozessor eine schnelle Anweisung für Absolutwerte hat (wie es der x86 tut), können Sie ein verzweigtes Min und Max ausführen, das schneller ist als ein if Anweisung oder ternäre Operation.

min(a,b) = (a + b - abs(a-b)) / 2
max(a,b) = (a + b + abs(a-b)) / 2

Wenn einer der Terme null ist (was beim Klemmen oft der Fall ist), vereinfacht sich der Code etwas weiter:

max(a,0) = (a + abs(a)) / 2

Wenn Sie beide Operationen kombinieren, können Sie die beiden ersetzen /2 zu einem einzigen /4 oder *0.25 um einen Schritt zu sparen.

Der folgende Code ist auf meinem Athlon II X2 über 3x schneller als ternär, wenn die Optimierung für FMIN=0 verwendet wird.

double clamp(double value)
{
    double temp = value + FMAX - abs(value-FMAX);
#if FMIN == 0
    return (temp + abs(temp)) * 0.25;
#else
    return (temp + (2.0*FMIN) + abs(temp-(2.0*FMIN))) * 0.25;
#endif
}

  • Wow – schöne Idee! Ich vermute, dass dies bei bestimmten CPUs/Compilern tatsächlich langsamer als ternär sein könnte, wenn abs(a) ist nicht schön inlined/optimiert…

    – Roddy

    16. September 2011 um 7:57 Uhr

  • In C# mit Math.Abs ​​ist dieser Ansatz langsamer.

    – Paul Tschernoch

    21. Oktober 2013 um 18:25 Uhr

  • Ich würde erwarten fabs(value-FMAX) statt int abs(int j).

    – chux – Wiedereinsetzung von Monica

    8. Januar 2016 um 17:52 Uhr

  • @chux Ich habe mit einem C++-Compiler getestet, der möglicherweise die richtige Funktion durch Überladen verwendet hat.

    – Markieren Sie Lösegeld

    8. Januar 2016 um 17:59 Uhr

  • Schwäche: Dieser Ansatz kann zu Präzisionsverlusten führen. FMAX Werte größer als value Genauigkeit im Ergebnis verlieren kann. Wenn FMAX ist 10x value, dann kann 1 Dezimalstelle verloren gehen. Im schlimmsten Fall ist der geklemmte Rückgabewert immer 0,0.

    – chux – Wiedereinsetzung von Monica

    8. Januar 2016 um 18:05 Uhr


Der ternäre Operator ist wirklich der richtige Weg, da die meisten Compiler in der Lage sind, sie in eine native Hardwareoperation zu kompilieren, die eine bedingte Verschiebung anstelle einer Verzweigung verwendet (und somit die Fehlvorhersage von Strafen und Pipeline-Blasen usw. vermeidet). Eine Bit-Manipulation verursacht wahrscheinlich ein Laden-Hit-Speichern.

Insbesondere PPC und x86 mit SSE2 haben eine Hardware-Operation, die wie folgt als intrinsisch ausgedrückt werden könnte:

double fsel( double a, double b, double c ) {
  return a >= 0 ? b : c; 
}

Der Vorteil ist, dass dies innerhalb der Pipeline geschieht, ohne eine Verzweigung zu verursachen. Wenn Ihr Compiler das Intrinsic verwendet, können Sie es tatsächlich verwenden, um Ihre Klemme direkt zu implementieren:

inline double clamp ( double a, double min, double max ) 
{
   a = fsel( a - min , a, min );
   return fsel( a - max, max, a );
}

Ich empfehle Ihnen dringend Vermeiden Sie die Bit-Manipulation von Doubles mit Integer-Operationen. Auf den meisten modernen CPUs gibt es keine direkte Möglichkeit, Daten zwischen Double- und Int-Registern zu verschieben, außer durch einen Roundtrip zum dcache. Dies führt zu einem Datenrisiko, das als Load-Hit-Store bezeichnet wird und die CPU-Pipeline im Wesentlichen leert, bis das Schreiben in den Speicher abgeschlossen ist (normalerweise etwa 40 Zyklen).

Die Ausnahme hiervon ist, wenn die Double-Werte bereits im Speicher und nicht in einem Register sind: In diesem Fall besteht keine Gefahr eines Load-Hit-Store. Ihr Beispiel zeigt jedoch, dass Sie gerade das Double berechnet und von einer Funktion zurückgegeben haben, was bedeutet, dass es sich wahrscheinlich immer noch in XMM1 befindet.

Roddys Benutzeravatar
Roddy

Für die 16.16-Darstellung ist es unwahrscheinlich, dass die einfache ternäre Geschwindigkeit in Bezug auf die Geschwindigkeit übertroffen wird.

Und für Doubles, weil Sie es Standard/Portable C brauchen, wird jede Art von Bit-Fummelei schlecht enden.

Selbst wenn ein bisschen Gefummel möglich wäre (was ich bezweifle), würden Sie sich auf die binäre Darstellung von Doubles verlassen. DIES (und ihre Größe) IST UMSETZUNGSABHÄNGIG.

Möglicherweise könnten Sie dies mit sizeof (double) “erraten” und dann das Layout verschiedener double-Werte mit ihren gemeinsamen binären Darstellungen vergleichen, aber ich denke, Sie verstecken sich vor nichts.

Die beste Regel lautet: SAGEN SIE DEM COMPILER, WAS SIE WOLLEN (dh ternär) und lassen Sie ihn für Sie optimieren.

BEARBEITEN: Bescheidene Kuchenzeit. Ich habe gerade die Idee von Quinmars (unten) getestet und es funktioniert – wenn Sie IEEE-754-Floats haben. Dies ergab eine Beschleunigung von etwa 20 % für den untenstehenden Code. Offensichtlich nicht portabel, aber ich denke, es gibt eine standardisierte Möglichkeit, Ihren Compiler zu fragen, ob er IEEE754-Float-Formate mit einem #IF verwendet …?

  double FMIN = 3.13;
  double FMAX = 300.44;

  double FVAL[10] = {-100, 0.23, 1.24, 3.00, 3.5, 30.5, 50 ,100.22 ,200.22, 30000};
  uint64  Lfmin = *(uint64 *)&FMIN;
  uint64  Lfmax = *(uint64 *)&FMAX;

    DWORD start = GetTickCount();

    for (int j=0; j<10000000; ++j)
    {
        uint64 * pfvalue = (uint64 *)&FVAL[0];
        for (int i=0; i<10; ++i)
            *pfvalue++ = (*pfvalue < Lfmin) ? Lfmin : (*pfvalue > Lfmax) ? Lfmax : *pfvalue;
    }

    volatile DWORD hacktime = GetTickCount() - start;

    for (int j=0; j<10000000; ++j)
    {
        double * pfvalue = &FVAL[0];
        for (int i=0; i<10; ++i)
            *pfvalue++ = (*pfvalue < FMIN) ? FMIN : (*pfvalue > FMAX) ? FMAX : *pfvalue;
    }

    volatile DWORD normaltime = GetTickCount() - (start + hacktime);

  • Angenommen, IEEE-754 für den Gleitkommafall ist für meine Situation portabel genug. Vielen Dank, dass Sie sich die Zeit genommen haben, dem nachzugehen.

    – Niklas

    9. Januar 2009 um 11:20 Uhr

  • Die Version mit int64_t wird falsche Ergebnisse liefern, wenn beide FMIN und *pfvalue kleiner als Null sind, z. B. FMIN=-1, FMAX=1, (*pfvalue)=-0,1; siehe meine Antwort stackoverflow.com/questions/427477/…

    – jfs

    9. Januar 2009 um 19:44 Uhr

  • @JFS Ah ja. IEE754 verwendet die Vorzeichen-/Größencodierung, nicht das Zweierkomplement. Vergleiche mit negativen Zahlen sind also fehlerhaft. Wenn sowohl FMIN als auch FMAX >= Null sind, ist alles in Ordnung (auch wenn pfvalue negativ ist). Wenn FMIN oder FMAX Null sind, sind alle Wetten ungültig …

    – Roddy

    9. Januar 2009 um 21:48 Uhr

  • Ich frage mich, ob Sie Zeit hätten, meine verzweigungsfreie Min/Max-Lösung mit Ihrer zu vergleichen? Ich würde mich über eine unabhängige Validierung freuen, zumal ich Ihre Ergebnisse nicht mit der Quinmars-Version duplizieren konnte.

    – Markieren Sie Lösegeld

    15. September 2011 um 13:08 Uhr

  • @ Mark – Ich werde sehen, was ich tun kann. Unterschiedliche Ergebnisse sind wahrscheinlich darauf zurückzuführen, dass Ihr Compiler gerade eine Last besser optimiert hat als meiner!

    – Roddy

    16. September 2011 um 7:59 Uhr

Die Bits des IEEE 754-Gleitkommas sind so geordnet, dass Sie beim Vergleich der als Ganzzahl interpretierten Bits dieselben Ergebnisse erhalten, als würden Sie sie direkt als Floats vergleichen. Wenn Sie also einen Weg finden oder kennen, Ganzzahlen zu klemmen, können Sie ihn auch für (IEEE 754) Floats verwenden. Tut mir leid, ich kenne keinen schnelleren Weg.

Wenn Sie die Floats in einem Array gespeichert haben, können Sie einige CPU-Erweiterungen wie SSE3 verwenden, wie rkj sagte. Sie können sich liboil ansehen, es erledigt die ganze Drecksarbeit für Sie. Hält Ihr Programm portabel und verwendet, wenn möglich, schnellere CPU-Anweisungen. (Ich bin mir nicht sicher, wie OS/Compiler-unabhängig liboil ist).

  • Angenommen, IEEE-754 für den Gleitkommafall ist für meine Situation portabel genug. Vielen Dank, dass Sie sich die Zeit genommen haben, dem nachzugehen.

    – Niklas

    9. Januar 2009 um 11:20 Uhr

  • Die Version mit int64_t wird falsche Ergebnisse liefern, wenn beide FMIN und *pfvalue kleiner als Null sind, z. B. FMIN=-1, FMAX=1, (*pfvalue)=-0,1; siehe meine Antwort stackoverflow.com/questions/427477/…

    – jfs

    9. Januar 2009 um 19:44 Uhr

  • @JFS Ah ja. IEE754 verwendet die Vorzeichen-/Größencodierung, nicht das Zweierkomplement. Vergleiche mit negativen Zahlen sind also fehlerhaft. Wenn sowohl FMIN als auch FMAX >= Null sind, ist alles in Ordnung (auch wenn pfvalue negativ ist). Wenn FMIN oder FMAX Null sind, sind alle Wetten ungültig …

    – Roddy

    9. Januar 2009 um 21:48 Uhr

  • Ich frage mich, ob Sie Zeit hätten, meine verzweigungsfreie Min/Max-Lösung mit Ihrer zu vergleichen? Ich würde mich über eine unabhängige Validierung freuen, zumal ich Ihre Ergebnisse nicht mit der Quinmars-Version duplizieren konnte.

    – Markieren Sie Lösegeld

    15. September 2011 um 13:08 Uhr

  • @ Mark – Ich werde sehen, was ich tun kann. Unterschiedliche Ergebnisse sind wahrscheinlich darauf zurückzuführen, dass Ihr Compiler gerade eine Last besser optimiert hat als meiner!

    – Roddy

    16. September 2011 um 7:59 Uhr

chux – Stellt Monicas Benutzeravatar wieder her
Chux – Wiedereinsetzung von Monica

Anstatt zu testen und zu verzweigen, verwende ich normalerweise dieses Format zum Klemmen:

clampedA = fmin(fmax(a,MY_MIN),MY_MAX);

Obwohl ich noch nie eine Leistungsanalyse des kompilierten Codes durchgeführt habe.

  • Nett. Jeder alternative Code sollte gegen diesen Standard getestet werden, um ihn in der Leistung zu schlagen, aber in der Funktionalität zu entsprechen.

    – chux – Wiedereinsetzung von Monica

    8. Januar 2016 um 19:40 Uhr


1407660cookie-checkDer schnellste Weg, um einen realen (Fest- / Gleitkomma-) Wert zu klemmen?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy