Wie führt man eine vorzeichenlose Sättigungsaddition in C durch?
Lesezeit: 12 Minuten
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
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.
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
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
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
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.
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:
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.
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):
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 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
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
14091300cookie-checkWie führt man eine vorzeichenlose Sättigungsaddition in C durch?yes
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