Wie führt man eine vorzeichenlose Sättigungsaddition in C durch?

Lesezeit: 12 Minuten

Benutzeravatar von Frank Szczerba
Frank Szczerba

Was ist der beste (sauberste, effizienteste) Weg, um eine Sättigungsaddition in C zu schreiben?

Die Funktion oder das Makro sollte zwei vorzeichenlose Eingaben hinzufügen (benötigt sowohl 16- als auch 32-Bit-Versionen) und alle Bits-Eins (0xFFFF oder 0xFFFFFFFF) zurückgeben, wenn die Summe überläuft.

Ziel ist x86 und ARM mit gcc (4.1.2) und Visual Studio (nur zur Simulation, daher ist eine Fallback-Implementierung dort in Ordnung).

  • Die Antwort von MSalters kompiliert zu bei weitem der beste Code auf x86, was dem Besten entspricht, was ich mit Inline-Asm tun kann (eigentlich besser, weil der Compiler versteht, was passiert, und auswählen kann, welcher Operand das Ziel der Addition sein wird). Es ist ähnlich ziemlich gut auf ARM. gcc scheint jedoch nicht ARMs Add mit unsignierter Sättigungsanweisung zu verwenden. Die Antwort von MSalters sollte die akzeptierte sein.

    – Peter Cordes

    13. März 2016 um 18:39 Uhr


  • Leider scheint der Sieg mit GCC 6 für die 16-Bit-adds16_msalters zu verschwinden, mit bedingten Sprüngen und allem.

    – Benutzer1649948

    17. Februar 2017 um 3:08 Uhr

  • Verwandt: vorzeichenbehaftete Sättigung: Vorzeichenbehaftete Addition von 64-Bit-Ganzzahlen? ist ein schwierigeres Problem. Meine Antwort dort benötigte eine integrierte GCC-Funktion, um effizient zu kompilieren. Im Gegensatz zum Carry-Flag ist es schwierig, Compiler dazu zu bringen, die Ausgabe des Signed-Overflow-Flags zu verwenden.

    – Peter Cordes

    11. Juni 2019 um 2:55 Uhr

Benutzeravatar von MSalters
MSalter

Wahrscheinlich möchten Sie hier portablen C-Code, den Ihr Compiler in eine richtige ARM-Assembly umwandelt. ARM hat bedingte Bewegungen, und diese können vom Überlauf abhängig sein. Der Algorithmus lautet dann: Füge das Ziel hinzu und setze es bedingt auf unsigned(-1), wenn ein Überlauf erkannt wurde.

uint16_t add16(uint16_t a, uint16_t b)
{
  uint16_t c = a + b;
  if (c < a)  /* Can only happen due to overflow */
    c = -1;
  return c;
}

Beachten Sie, dass sich dies von den anderen Algorithmen darin unterscheidet, dass es einen Überlauf korrigiert, anstatt sich auf eine andere Berechnung zu verlassen, um einen Überlauf zu erkennen.

x86-64 clang 3.7 -O3-Ausgabe für adds32: deutlich besser als jede andere Antwort:

add     edi, esi
mov     eax, -1
cmovae  eax, edi
ret

ARMv7: gcc 4.8 -O3 -mcpu=cortex-a15 -fverbose-asm Ausgabe für adds32:

adds    r0, r0, r1      @ c, a, b
it      cs
movcs   r0, #-1         @ conditional-move
bx      lr

16bit: verwendet immer noch nicht den unsigned-saturating add-Befehl von ARM (UADD16)

add     r1, r1, r0        @ tmp114, a
movw    r3, #65535      @ tmp116,
uxth    r1, r1  @ c, tmp114
cmp     r0, r1    @ a, c
ite     ls        @
movls   r0, r1        @,, c
movhi   r0, r3        @,, tmp116
bx      lr  @

  • Dies generiert optimalen Code auf x86 mit clang (mov eax,-1 / add / cmovnc), und ungefähr das gleiche mit gcc, im Gegensatz zu allen anderen Antworten. Es ist das einzige, das gcc dazu bringt, das Flag-Ergebnis aus dem Hinzufügen zu verwenden, anstatt danach einen weiteren Test durchzuführen (mit Ausnahme der Antwort von DEntry, aber gcc erkennt nicht, dass beide Tests gleich sind). Man könnte also sagen, es ist das einzige, wo gcc “versteht”, was vor sich geht. Selbst Inline-ASM kann auf x86 nicht besser werden: Der Compiler weiß, was mit Ihrem los ist, also weiß er, dass es assoziativ ist, und kann wählen, welche Registrierung zerstört werden soll.

    – Peter Cordes

    13. März 2016 um 17:43 Uhr


  • @PeterCordes: Möchten Sie das Verhalten neuerer clang/gcc-Versionen kommentieren? Seit clang 3.9 und gcc 6.1 wird die 16-Bit-Version deutlich unhandlicher. Ich habe Clang davon überzeugt, denselben Code zu erzeugen, den Sie durch Deaktivieren zeigen likely aber gcc scheint beharrlicher zu sein. Die 32-Bit-Versionen funktionieren wie erwartet (wiederum wahrscheinlich für Clang deaktivieren), aber ich brauche eine 16-Bit-Sättigungserweiterung.

    – Rici

    9. März 2018 um 15:42 Uhr

  • @rici: Wenn der Compiler bei 16-Bit ohne Vorzeichen bereits über Werte verfügt, die in Registern um Null erweitert wurden, ist es möglicherweise optimal, eine 32-Bit-Addition durchzuführen und nur zu überprüfen sum & (1UL<<16) zum Mitnehmen. Compiler machen damit (keineswegs) optimale Arbeit, aber die verzweigte Version von clang6.0 ist interessant, wenn der Normalfall kein Überlauf ist. godbolt.org/g/qrpPze. (Es sollte verwenden lea zum Kopieren und Hinzufügen.) Wenn Teilregister-Stalls für 16-Bit-Regs nicht vorhanden sind (wie bei Haswell), sieht die verzweigte Version dieser Antwort von Clang auch in Ordnung aus, aber gccs hat einen dummen Test (fehlende Optimierung gemeldet werden soll).

    – Peter Cordes

    10. März 2018 um 2:11 Uhr

  • Diese können beim Inlining anders ausfallen; Das Branch-Layout wäre sehr wahrscheinlich anders, wenn es sich nicht nur um eine eigenständige Funktion handelt.

    – Peter Cordes

    10. März 2018 um 2:11 Uhr

  • @peter: Mein eigentlicher Anwendungsfall ist das Vergleichen z < clamped_subtract(h, 4) woz ist ein size_t und h ist ein uint16_t. Der vorhandene Code ist z + 4 < haber das schlägt natürlich fehl, wenn die Addition überläuft (sehr unwahrscheinlich, aber es ist ein Fehler und ich würde ihn gerne beheben. Er befindet sich nicht in einem kritischen Pfad, also mache ich mir keine allzu großen Sorgen, aber ich habe nachgesehen, ob es etwas gibt besser als zwei Vergleiche.

    – Rici

    10. März 2018 um 2:44 Uhr

Benutzeravatar von Remo.D
Remo.D

In normalem C:

uint16_t sadd16(uint16_t a, uint16_t b) {
  return (a > 0xFFFF - b) ? 0xFFFF : a + b;
}
     
uint32_t sadd32(uint32_t a, uint32_t b) {
  return (a > 0xFFFFFFFF - b) ? 0xFFFFFFFF : a + b;
}

die fast makroisiert ist und die Bedeutung direkt vermittelt.

  • Nett. Ein Nitpick – wenn ich den Namen gesehen habe sadd16 in einigen Code wäre meine erste Annahme, dass die s steht für signed.

    – Craig McQueen

    3. Juli 2010 um 12:51 Uhr

  • @Anonymous: Craig spricht vom Standpunkt des Lesens von Code, wo es einen Aufruf an sad16/32 gibt. Sie werden die Signatur nur sehen, wenn Sie die Kopfzeile finden und öffnen.

    – Josef Garwin

    26. März 2012 um 15:18 Uhr

  • @DietrichEpp Fair genug. Ich werde nicht hier sitzen und einen Vortrag über etwas halten, das ich bereits weiß. Allerdings, ein Clever Compiler würde nicht Inline-Funktionen, auch wenn sie im Debug-Modus dazu gezwungen werden. Ein Beispiel ist MSVC. Wenn Sie es dem Compiler für den Debug-Modus mitteilen, werden keine (auch nicht erzwungenen) Funktionen eingebunden.

    – Cole Tobin

    17. Juni 2013 um 3:55 Uhr

  • @Dietrich Das ist dumm. Ich denke, ich habe es nie bemerkt, weil ich in MSVC arbeite und dann auf GCC portiere, wenn ich fertig bin.

    – Cole Tobin

    17. Juni 2013 um 8:29 Uhr

  • Nur ein kleiner Vorschlag: Die 0xFF.. Konstanten sollten in das Äquivalent geändert werden UINTN_MAX Konstanten (bzw (uintN_t) -1). Auf diese Weise ist nur ein einziges Suchen und Ersetzen erforderlich, um die zu schreiben sadd8 oder sadd64 Funktionen. (Und Sie müssen die Anzahl der Fs nicht zählen 0xFFFFFFFFFFFFFFFF 😉

    – Alexandros

    4. Dezember 2013 um 8:52 Uhr


Benutzeravatar von Skizz
Schizz

In IA32 ohne bedingte Sprünge:

uint32_t sadd32(uint32_t a, uint32_t b)
{
#if defined IA32
  __asm
  {
    mov eax,a
    xor edx,edx
    add eax,b
    setnc dl
    dec edx
    or eax,edx
  }
#elif defined ARM
  // ARM code
#else
  // non-IA32/ARM way, copy from above
#endif
}

  • Wenn die Frage Portabilität wollte, hätte sie nicht x86 und ARM angeben sollen 😉

    – Steve Jessop

    23. September 2008 um 14:40 Uhr

  • Diese Funktion ist immer noch portabel – sobald die Elif- und Else-Fälle ausgefüllt sind. Portabler Code bedeutet nicht, dass Sie ihn nicht für bestimmte Plattformen optimieren können.

    – Arafangion

    18. Mai 2010 um 2:53 Uhr

  • Eine vorgeschlagene Bearbeitung von YumeYao (die ich nicht durchgesetzt habe, da sie die Art der Antwort ändert): Die 3 Anweisungen (xor reg,reg; setne reg; dec reg;) können durch eine effizientere Anweisung (sbb reg, reg).

    – Marc Kies

    22. August 2011 um 8:13 Uhr

  • Zwei Dinge: die __asm Schlüsselwort ist Compiler-abhängig. Der Standard gibt kein Schlüsselwort für die Inline-Assemblierung an. Das ist also nicht portabel in dem Sinne, dass es vom Compiler abhängig ist. Beispielsweise ist der Intel C++-Compiler nur für Windows verfügbar. Wenn Sie also portablen Code unter Verwendung von Itel C++-Funktionen geschrieben haben, wäre er nicht portabel. Eine andere Sache: Inline-Assemblierung verhindert Compiler-Inlining. Diese Optimierung hilft also nicht wirklich, wenn noch der Funktionsaufruf-Overhead vorhanden ist …

    – Cole Tobin

    16. Juni 2013 um 21:51 Uhr

  • Das ist irgendwie scheiße: Erstens, weil es MSVC-Inline-asm ist, also müssen Ein- und Ausgänge durch den Speicher gehen. (Oder wenn diese No-Return-Anweisung mit einem Wert in eax funktioniert, dann kann die Funktion selbst nicht inline. Die Eingaben müssen trotzdem durch den Speicher gehen). Zweitens, weil cmov besser: kürzerer kritischer Pfad, weil mov eax, -1 ist abseits des kritischen Pfades, im Gegensatz zu sbb.

    – Peter Cordes

    13. März 2016 um 18:28 Uhr

Benutzeravatar von Nils Pipenbrinck
Nils Pipenbrink

In ARM ist möglicherweise bereits gesättigte Arithmetik integriert. Die ARMv5 DSP-Erweiterungen können Register auf jede Bitlänge sättigen. Auch auf ARM ist die Sättigung meist günstig, da man die meisten Befehle bedingt ausführen kann.

ARMv6 hat sogar gesättigte Addition, Subtraktion und all das andere Zeug für 32 Bit und gepackte Zahlen.

Auf dem x86 erhalten Sie entweder über MMX oder SSE gesättigte Arithmetik.

All dies erfordert Assembler, also ist es nicht das, wonach Sie gefragt haben.

Es gibt auch C-Tricks für gesättigte Arithmetik. Dieser kleine Code führt eine gesättigte Addition von vier Bytes eines Doppelworts durch. Es basiert auf der Idee, 32 Halbaddierer parallel zu berechnen, also zB Zahlen ohne Übertrag zu addieren.

Dies wird zuerst erledigt. Dann werden die Überträge berechnet, addiert und durch eine Maske ersetzt, falls die Addition überlaufen würde.

uint32_t SatAddUnsigned8(uint32_t x, uint32_t y) 
{
  uint32_t signmask = 0x80808080;
  uint32_t t0 = (y ^ x) & signmask;
  uint32_t t1 = (y & x) & signmask;
  x &= ~signmask;
  y &= ~signmask;
  x += y;
  t1 |= t0 & x;
  t1 = (t1 << 1) - (t1 >> 7);
  return (x ^ t0) | t1;
}

Sie können dasselbe für 16 Bit (oder jede Art von Bitfeld) erhalten, indem Sie die Zeichenmaskenkonstante und die Verschiebungen unten wie folgt ändern:

uint32_t SatAddUnsigned16(uint32_t x, uint32_t y) 
{
  uint32_t signmask = 0x80008000;
  uint32_t t0 = (y ^ x) & signmask;
  uint32_t t1 = (y & x) & signmask;
  x &= ~signmask;
  y &= ~signmask;
  x += y;
  t1 |= t0 & x;
  t1 = (t1 << 1) - (t1 >> 15);
  return (x ^ t0) | t1;
}

uint32_t SatAddUnsigned32 (uint32_t x, uint32_t y)
{
  uint32_t signmask = 0x80000000;
  uint32_t t0 = (y ^ x) & signmask;
  uint32_t t1 = (y & x) & signmask;
  x &= ~signmask;
  y &= ~signmask;
  x += y;
  t1 |= t0 & x;
  t1 = (t1 << 1) - (t1 >> 31);
  return (x ^ t0) | t1;
}

Der obige Code macht dasselbe für 16- und 32-Bit-Werte.

Wenn Sie die Funktion nicht benötigen, dass die Funktionen mehrere Werte parallel addieren und sättigen, maskieren Sie einfach die benötigten Bits. Auf ARM möchten Sie auch die Signmask-Konstante ändern, da ARM nicht alle möglichen 32-Bit-Konstanten in einem einzigen Zyklus laden kann.

Bearbeiten: Die parallelen Versionen sind höchstwahrscheinlich langsamer als die direkten Methoden, aber sie sind schneller, wenn Sie mehr als einen Wert gleichzeitig sättigen müssen.

Benutzeravatar von Dark Shikari
Dunkle Shikari

Wenn Sie Wert auf Leistung legen, Sie Ja wirklich Ich möchte solche Sachen in SIMD machen, wo x86 eine native Sättigungsarithmetik hat.

Aufgrund dieses Mangels an sättigender Arithmetik in der Skalarmathematik kann es Fälle geben, in denen Operationen auf SIMD mit 4 Variablen durchgeführt werden mehr als 4-mal schneller als das entsprechende C (und entsprechend wahr mit 8-Variablen-breitem SIMD):

sub8x8_dct8_c: 1332 clocks
sub8x8_dct8_mmx: 182 clocks
sub8x8_dct8_sse2: 127 clocks

  • Ist die Verwendung der SSE-Anweisungen immer noch schneller, wenn Sie immer nur eine Variable gleichzeitig bearbeiten?

    – Josef Garwin

    19. März 2012 um 16:05 Uhr

  • @JosephGarvin: ja, es kann sein, wenn Sie 16-Bit- oder 8-Bit-Sättigung benötigen, addieren oder subtrahieren. Oder Bit-Reverse (mit SSSE3 pshufb für eine parallele Nachschlagetabelle pro Nibble). Oder mit SSE4.1 min oder max auf 32-Bit-Ganzzahlen (oder abs) mit einer einzigen Anweisung. Oder 64-Bit-Integer-Mathematik in 32-Bit-Code. Aber es gibt Overhead, Zahlen zwischen XMM- und Integer-Registern zu bekommen, also mit Vorsicht verwenden.

    – Peter Cordes

    11. Juni 2019 um 2:41 Uhr


R.. GitHub STOP HELPING ICEs Benutzeravatar
R.. GitHub HÖREN SIE AUF, ICE ZU HELFEN

Zero-Branch-Lösung:

uint32_t sadd32(uint32_t a, uint32_t b)
{
    uint64_t s = (uint64_t)a+b;
    return -(s>>32) | (uint32_t)s;
}

Ein guter Compiler wird dies optimieren, um zu vermeiden, dass tatsächlich 64-Bit-Arithmetik durchgeführt wird (s>>32 wird lediglich das Carry-Flag sein, und -(s>>32) ist das Ergebnis von sbb %eax,%eax).

In x86 asm (AT&T-Syntax, a und b in eax und ebxergeben eax):

add %eax,%ebx
sbb %eax,%eax
or %ebx,%eax

8- und 16-Bit-Versionen sollten offensichtlich sein. Die signierte Version erfordert möglicherweise etwas mehr Arbeit.

  • Ist die Verwendung der SSE-Anweisungen immer noch schneller, wenn Sie immer nur eine Variable gleichzeitig bearbeiten?

    – Josef Garwin

    19. März 2012 um 16:05 Uhr

  • @JosephGarvin: ja, es kann sein, wenn Sie 16-Bit- oder 8-Bit-Sättigung benötigen, addieren oder subtrahieren. Oder Bit-Reverse (mit SSSE3 pshufb für eine parallele Nachschlagetabelle pro Nibble). Oder mit SSE4.1 min oder max auf 32-Bit-Ganzzahlen (oder abs) mit einer einzigen Anweisung. Oder 64-Bit-Integer-Mathematik in 32-Bit-Code. Aber es gibt Overhead, Zahlen zwischen XMM- und Integer-Registern zu bekommen, also mit Vorsicht verwenden.

    – Peter Cordes

    11. Juni 2019 um 2:41 Uhr


Benutzeravatar von DGentry
DGentry

uint32_t saturate_add32(uint32_t a, uint32_t b)
{
    uint32_t sum = a + b;
    if ((sum < a) || (sum < b))
        return ~((uint32_t)0);
    else
        return sum;
} /* saturate_add32 */

uint16_t saturate_add16(uint16_t a, uint16_t b)
{
    uint16_t sum = a + b;
    if ((sum < a) || (sum < b))
        return ~((uint16_t)0);
    else
        return sum;
} /* saturate_add16 */

Bearbeiten: Jetzt, da Sie Ihre Version gepostet haben, bin ich mir nicht sicher, ob meine sauberer/besser/effizienter/studierter ist.

  • Ihre Antwort sieht aus wie das, was ich dachte, was wir tun sollten, aber wie Sie sagten, bin ich mir nicht sicher, was besser ist, weshalb ich dachte, ich würde hier abstimmen.

    – Frank Szczerba

    23. September 2008 um 14:23 Uhr

  • Sie scheinen beide richtig zu sein, daher sollte die Effizienz entscheiden. Ein zusätzlicher Vergleich ist nicht offensichtlich langsamer (oder schneller) als eine Überdimensionierung der Addition. Führen Sie einige Effizienztests für beide Lösungen auf beiden Architekturen durch und wählen Sie die schnellere aus.

    – Rafal Dowgird

    23. September 2008 um 14:27 Uhr

  • Ist die Überprüfung der Summe gegen beide Eingaben erforderlich? Der Grenzfall ist (uint16_t)(0xffff + 1), was sowohl < 1 als auch < 0xffff ist, sodass die zweite Prüfung anscheinend vermieden werden kann.

    – Frank Szczerba

    23. September 2008 um 15:05 Uhr

  • Sie haben Recht, das verlorene Überlaufbit ist MAXINT+1 wert, also ist das Ergebnis der Überlaufaddition gleich a+b-(MAXINT+1), was sowohl kleiner als a als auch kleiner als b ist.

    – Rafal Dowgird

    23. September 2008 um 20:07 Uhr

  • Warum verwenden ~((uint32_t)0)? Sie schließen bereits ein <limits.h> um das zu bekommen uint32_t Entschleunigung, also warum nicht einfach nutzen UINT32_MAX?

    – Cole Tobin

    16. Juni 2013 um 21:52 Uhr

1409130cookie-checkWie führt man eine vorzeichenlose Sättigungsaddition in C durch?

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

Privacy policy