Gibt es einige aussagekräftige statistische Daten, um zu rechtfertigen, dass der arithmetische Überlauf von vorzeichenbehafteten Ganzzahlen undefiniert bleibt?

Lesezeit: 5 Minuten

Benutzeravatar von chqrlie
chqrlie

Der C-Standard gibt ausdrücklich einen Überlauf von vorzeichenbehafteten Ganzzahlen an undefiniertes Verhalten. Die meisten CPUs implementieren jedoch vorzeichenbehaftete Arithmetik mit definierter Semantik für den Überlauf (außer vielleicht für den Divisionsüberlauf: x / 0 und INT_MIN / -1).

Compiler-Autoren haben sich die Vorteile zunutze gemacht Unbestimmtheit solcher Überläufe, um aggressivere Optimierungen hinzuzufügen, die dazu neigen, Legacy-Code auf sehr subtile Weise zu beschädigen. Beispielsweise hat dieser Code möglicherweise auf älteren Compilern funktioniert, funktioniert aber nicht mehr auf aktuellen Versionen von gcc und clang:

/* Tncrement a by a value in 0..255, clamp a to positive integers.
   The code relies on 32-bit wrap-around, but the C Standard makes
   signed integer overflow undefined behavior, so sum_max can now 
   return values less than a. There are Standard compliant ways to
   implement this, but legacy code is what it is... */
int sum_max(int a, unsigned char b) {
    int res = a + b;
    return (res >= a) ? res : INT_MAX;
}

Gibt es handfeste Beweise dafür, dass sich diese Optimierungen lohnen? Gibt es vergleichende Studien, die die tatsächlichen Verbesserungen an realen Beispielen oder sogar an klassischen Benchmarks dokumentieren?

Mir kam diese Frage, als ich mir das ansah: C++Now 2018: John Regehr „Closing Keynote: Undefined Behavior and Compiler Optimizations“

Ich tagge c und c++ da das Problem in beiden Sprachen ähnlich ist, die Antworten jedoch unterschiedlich sein können.

  • Kommentare sind nicht für längere Diskussionen gedacht; diese Konversation wurde in den Chat verschoben.

    – Samuel Liew

    8. Mai 2019 um 23:43 Uhr

  • Der Grund, warum C sagt, dass der Überlauf von vorzeichenbehafteten Ganzzahlen undefiniert ist, ist, dass einige CPUs das “2er-Komplement” verwenden, einige das “1er-Komplement”, einige “Vorzeichen und Größe” verwenden; und für alle Fälle könnte ein Überlauf alles verursachen (z. B. CPUs wie MIPS haben “Trap on Overflow”). Mit anderen Worten, es geht um Portabilität und nicht um Optimierung.

    – Brenda

    8. Mai 2019 um 23:53 Uhr

  • Exakt. Die einzige „aussagekräftige Statistik“, die jeder braucht, ist, dass es Computer mit Einerkomplement und Vorzeichengröße gibt.

    – Benutzer207421

    9. Mai 2019 um 1:50 Uhr

  • @ user207421: Ja, das ist eine gute Frage, auf die die Antwort zu sein scheint nicht mehr, nicht länger. Daher der aktuelle Vorschlag, die Unterstützung für Nicht-Zweierkomplementdarstellungen zu entfernen: open-std.org/jtc1/sc22/wg14/www/docs/n2330.pdf

    – chqrlie

    9. Mai 2019 um 5:16 Uhr


  • @chqrlie: C gehört auch der Vergangenheit an (jetzt 47 Jahre alt). Es gibt viele Designentscheidungen, die damals sinnvoll waren, die heute keinen Sinn mehr machen, aber weiterhin existieren, weil Änderungen zu viel vorhandene Software zerstören würden.

    – Brenda

    9. Mai 2019 um 20:41 Uhr

bolovs Benutzeravatar
Bolov

Ich kenne mich mit Studien und Statistiken nicht aus, aber ja, es gibt definitiv Optimierungen, die dies berücksichtigen, die Compiler tatsächlich durchführen. Und ja, sie sind sehr wichtig (z. B. Tldr-Loop-Vektorisierung).

Neben den Compiler-Optimierungen ist noch ein weiterer Aspekt zu beachten. Mit UB erhalten Sie C/C++ signierte Integer, die sich arithmetisch so verhalten, wie Sie es mathematisch erwarten würden. Zum Beispiel x + 10 > x gilt jetzt (für gültigen Code natürlich), würde aber nicht auf ein Wrap-Around-Verhalten hinweisen.

Ich habe einen ausgezeichneten Artikel gefunden Wie undefinierter signierter Überlauf Optimierungen in GCC ermöglicht aus dem Blog von Krister Walfridsson, der einige Optimierungen auflistet, die den signierten Überlauf UB berücksichtigen. Die folgenden Beispiele stammen daraus. Ich füge ihnen c++ und Assembly-Beispiele hinzu.

Wenn die Optimierungen zu einfach, uninteressant oder wirkungslos aussehen, denken Sie daran, dass diese Optimierungen nur Schritte in einer viel, viel größeren Kette von Optimierungen sind. Und der Schmetterlingseffekt tritt tatsächlich auf, da eine scheinbar unwichtige Optimierung in einem früheren Schritt eine viel wirkungsvollere Optimierung in einem späteren Schritt auslösen kann.

Wenn die Beispiele unsinnig aussehen (wer würde schreiben x * 10 > 0) denken Sie daran, dass Sie diese Art von Beispielen in C und C++ sehr einfach mit Konstanten, Makros und Vorlagen erreichen können. Außerdem kann der Compiler auf diese Art von Beispielen zugreifen, wenn er Transformationen und Optimierungen in seinem IR anwendet.

Vereinfachung von vorzeichenbehafteten ganzzahligen Ausdrücken

  • Multiplikation im Vergleich zu 0 eliminieren

    (x * c) cmp 0   ->   x cmp 0 
    
    bool foo(int x) { return x * 10 > 0 }
    
    foo(int):
            test    edi, edi
            setg    al
            ret
    
  • Eliminiere die Division nach der Multiplikation

    (x * c1) / c2 -> x * (c1 / c2) wenn c1 durch c2 teilbar ist

    int foo(int x) { return (x * 20) / 10; }
    
    foo(int):
            lea     eax, [rdi+rdi]
            ret
    
  • Verneinung eliminieren

    (-x) / (-y) -> x / y

    int foo(int x, int y) { return (-x) / (-y); }
    
    foo(int, int):
            mov     eax, edi
            cdq
            idiv    esi
            ret
    
  • Vereinfachen Sie Vergleiche, die immer wahr oder falsch sind

    x + c < x       ->   false
    x + c <= x      ->   false
    x + c > x       ->   true
    x + c >= x      ->   true
    
    bool foo(int x) { return x + 10 >= x; }
    
    foo(int):
            mov     eax, 1
            ret
    
  • Beseitigen Sie die Negation in Vergleichen

    (-x) cmp (-y)   ->   y cmp x
    
    bool foo(int x, int y) { return -x < -y; }
    
    foo(int, int):
            cmp     edi, esi
            setg    al
            ret
    
  • Reduzieren Sie die Größe der Konstanten

    x + c > y       ->   x + (c - 1) >= y
    x + c <= y      ->   x + (c - 1) < y
    
    bool foo(int x, int y) { return x + 10 <= y; }
    
    foo(int, int):
            add     edi, 9
            cmp     edi, esi
            setl    al
            ret
    
  • Eliminiere Konstanten in Vergleichen

    (x + c1) cmp c2         ->   x cmp (c2 - c1)
    (x + c1) cmp (y + c2)   ->   x cmp (y + (c2 - c1)) if c1 <= c2
    

    Die zweite Transformation ist nur gültig, wenn c1 <= c2, da sie sonst einen Überlauf einführen würde, wenn y den Wert INT_MIN hat.

    bool foo(int x) { return x + 42 <= 11; }
    
    foo(int):
            cmp     edi, -30
            setl    al
            ret
    

Zeigerarithmetik und Typumwandlung

Wenn eine Operation nicht überläuft, erhalten wir das gleiche Ergebnis, wenn wir die Operation in einem breiteren Typ ausführen. Dies ist oft nützlich, wenn Sie Dinge wie die Array-Indizierung auf 64-Bit-Architekturen durchführen – die Indexberechnungen werden normalerweise mit 32-Bit-Int durchgeführt, aber die Zeiger sind 64-Bit, und der Compiler generiert möglicherweise effizienteren Code, wenn der vorzeichenbehaftete Überlauf nicht durch definiert ist Heraufstufen der 32-Bit-Ganzzahlen in 64-Bit-Operationen, anstatt Typerweiterungen zu generieren.

Ein weiterer Aspekt dabei ist, dass ein undefinierter Überlauf dafür sorgt, dass a[i]
und ein[i+1] sind benachbart. Dies verbessert die Analyse von Speicherzugriffen für die Vektorisierung etc.

Dies ist eine sehr wichtige Optimierung, da die Schleifenvektorisierung einer der effizientesten und effektivsten Optimierungsalgorithmen ist.

Dies ist ein Beispiel, wenn das Ändern eines Index von einem unsignierten Index zu einem signierten Index die generierte Assembly verbessert:

Unsignierte Version

#include <cstddef>

auto foo(int* v, std::size_t start)
{
    int sum = 0;

    for (std::size_t i = start; i < start + 4; ++i)
        sum += v[i];

    return sum;
}

Mit unsigned der Fall, wo start + 4 Umbrüche müssen berücksichtigt werden, und es wird eine Verzweigung generiert, um diesen Fall zu behandeln (Verzweigungen sind schlecht für die Leistung):

; gcc on x64 with -march=skylake

foo1(int*, unsigned long):
        cmp     rsi, -5
        ja      .L3
        vmovdqu xmm0, XMMWORD PTR [rdi+rsi*4]
        vpsrldq xmm1, xmm0, 8
        vpaddd  xmm0, xmm0, xmm1
        vpsrldq xmm1, xmm0, 4
        vpaddd  xmm0, xmm0, xmm1
        vmovd   eax, xmm0
        ret
.L3:
        xor     eax, eax
        ret
; clang on x64 with -march=skylake

foo1(int*, unsigned long):                             # @foo1(int*, unsigned long)
        xor     eax, eax
        cmp     rsi, -4
        jae     .LBB0_2
        vpbroadcastq    xmm0, qword ptr [rdi + 4*rsi + 8]
        vpaddd  xmm0, xmm0, xmmword ptr [rdi + 4*rsi]
        vpshufd xmm1, xmm0, 85                  # xmm1 = xmm0[1,1,1,1]
        vpaddd  xmm0, xmm0, xmm1
        vmovd   eax, xmm0
.LBB0_2:
        ret

Als Nebenbemerkung würde die Verwendung eines schmaleren Typs zu einer noch schlechteren Assemblierung führen und die Verwendung von SSE-vektorisierten Anweisungen verhindern:

#include <cstddef>

auto foo(int* v, unsigned start)
{
    int sum = 0;

    for (unsigned i = start; i < start + 4; ++i)
        sum += v[i];

    return sum;
}
; gcc on x64 with -march=skylake

foo(int*, unsigned int):
        cmp     esi, -5
        ja      .L3
        mov     eax, esi
        mov     eax, DWORD PTR [rdi+rax*4]
        lea     edx, [rsi+1]
        add     eax, DWORD PTR [rdi+rdx*4]
        lea     edx, [rsi+2]
        add     eax, DWORD PTR [rdi+rdx*4]
        lea     edx, [rsi+3]
        add     eax, DWORD PTR [rdi+rdx*4]
        ret
.L3:
        xor     eax, eax
        ret
; clang on x64 with -march=skylake

foo(int*, unsigned int):                              # @foo(int*, unsigned int)
        xor     eax, eax
        cmp     esi, -5
        ja      .LBB0_3
        mov     ecx, esi
        add     esi, 4
        mov     eax, dword ptr [rdi + 4*rcx]
        lea     rdx, [rcx + 1]
        cmp     rdx, rsi
        jae     .LBB0_3
        add     eax, dword ptr [rdi + 4*rcx + 4]
        add     eax, dword ptr [rdi + 4*rcx + 8]
        add     eax, dword ptr [rdi + 4*rcx + 12]
.LBB0_3:
        ret

Signierte Fassung

Die Verwendung eines signierten Index führt jedoch zu einem schönen vektorisierten Code ohne Zweige:

#include <cstddef>

auto foo(int* v, std::ptrdiff_t start)
{
    int sum = 0;

    for (std::ptrdiff_t i = start; i < start + 4; ++i)
        sum += v[i];

    return sum;
}
; gcc on x64 with -march=skylake

foo(int*, long):
        vmovdqu xmm0, XMMWORD PTR [rdi+rsi*4]
        vpsrldq xmm1, xmm0, 8
        vpaddd  xmm0, xmm0, xmm1
        vpsrldq xmm1, xmm0, 4
        vpaddd  xmm0, xmm0, xmm1
        vmovd   eax, xmm0
        ret
; clang on x64 with -march=skylake

foo(int*, long):                              # @foo(int*, long)
        vpbroadcastq    xmm0, qword ptr [rdi + 4*rsi + 8]
        vpaddd  xmm0, xmm0, xmmword ptr [rdi + 4*rsi]
        vpshufd xmm1, xmm0, 85                  # xmm1 = xmm0[1,1,1,1]
        vpaddd  xmm0, xmm0, xmm1
        vmovd   eax, xmm0
        ret

Vektorisierte Anweisungen werden immer noch verwendet, wenn ein schmalerer vorzeichenbehafteter Typ verwendet wird:

#include <cstddef>

auto foo(int* v, int start)
{
    int sum = 0;

    for (int i = start; i < start + 4; ++i)
        sum += v[i];

    return sum;
}
; gcc on x64 with -march=skylake

foo(int*, int):
        movsx   rsi, esi
        vmovdqu xmm0, XMMWORD PTR [rdi+rsi*4]
        vpsrldq xmm1, xmm0, 8
        vpaddd  xmm0, xmm0, xmm1
        vpsrldq xmm1, xmm0, 4
        vpaddd  xmm0, xmm0, xmm1
        vmovd   eax, xmm0
        ret
; clang on x64 with -march=skylake

foo(int*, int):                              # @foo(int*, int)
        movsxd  rax, esi
        vpbroadcastq    xmm0, qword ptr [rdi + 4*rax + 8]
        vpaddd  xmm0, xmm0, xmmword ptr [rdi + 4*rax]
        vpshufd xmm1, xmm0, 85                  # xmm1 = xmm0[1,1,1,1]
        vpaddd  xmm0, xmm0, xmm1
        vmovd   eax, xmm0
        ret

Wertebereichsberechnungen

Der Compiler verfolgt den Bereich der möglichen Werte der Variablen an jedem Punkt im Programm, dh für Code wie z

int x = foo();
if (x > 0) {
  int y = x + 5;
  int z = y / 4;

es bestimmt, dass x den Bereich hat [1, INT_MAX] nach der if-Anweisung und kann somit feststellen, dass y den Wertebereich hat [6, INT_MAX] da Überlauf nicht erlaubt ist. Und die nächste Zeile kann optimiert werden int z = y >> 2; da der Compiler weiß, dass y nicht negativ ist.

auto foo(int x)
{
    if (x <= 0)
        __builtin_unreachable();
    
    return (x + 5) / 4;
}
foo(int):
        lea     eax, [rdi+5]
        sar     eax, 2
        ret

Der undefinierte Überlauf hilft bei Optimierungen, die zwei Werte vergleichen müssen (da der Verpackungsfall mögliche Werte des Formulars ergeben würde
[INT_MIN, (INT_MIN+4)] oder [6, INT_MAX] das verhindert alle sinnvollen Vergleiche mit < oder >), wie zum Beispiel

  • Wechselnde Vergleiche x<y auf wahr oder falsch, wenn die Bereiche für x und y überschneidet sich nicht
  • Ändern min(x,y) oder max(x,y) zu x oder y wenn sich die Bereiche nicht überschneiden
  • Ändern abs(x) zu x oder -x wenn sich der Bereich nicht kreuzt 0
  • Ändern x/c zu x>>log2(c) wenn x>0 und die Konstante c ist eine Macht von 2
  • Ändern x%c zu x&(c-1) wenn x>0 und die Konstante c ist eine Macht von 2

Schleifenanalyse und -optimierung

Das kanonische Beispiel dafür, warum ein undefinierter vorzeichenbehafteter Überlauf Schleifenoptimierungen unterstützt, ist, dass Schleifen wie

for (int i = 0; i <= m; i++)

werden bei undefiniertem Überlauf garantiert beendet. Dies hilft Architekturen mit spezifischen Schleifenanweisungen, da sie im Allgemeinen keine Endlosschleifen verarbeiten.

Aber ein undefinierter vorzeichenbehafteter Überlauf hilft bei vielen weiteren Schleifenoptimierungen. Alle Analysen wie das Bestimmen der Anzahl der Iterationen, das Transformieren von Induktionsvariablen und das Verfolgen von Speicherzugriffen verwenden alles in den vorherigen Abschnitten, um ihre Arbeit zu erledigen. Im Speziellen, Die Menge der Schleifen, die vektorisiert werden können, wird stark reduziert, wenn ein vorzeichenbehafteter Überlauf zugelassen wird.

  • Sehr informative Antwort (der Blog ist in der Tat eine gute Lektüre). Da all diese Optimierungen nur für vorzeichenbehaftete Operanden möglich sind, ist die Schlussfolgerung kontraintuitiv: using size_t Indexwerte für viele Berechnungen sollten aufgrund der Modulo-Arithmetik-Semantik zu weniger effizientem Code führen. Dies ist ein gutes Argument, das Sie einbeziehen sollten ssize_t im C-Standard oder ein Typ ohne Vorzeichen ohne Modulo-Arithmetik.

    – chqrlie

    9. Mai 2019 um 5:50 Uhr

  • @immibis Ich meinte, es gilt für allen gültigen Code. UB ist kein gültiger Code.

    – Bolov

    9. Mai 2019 um 9:41 Uhr

  • @chqrlie Ich habe auf cppcon gehört, dass prominente Mitglieder des Standardkomitees anerkannt haben, dass die nicht signierte Größe im Nachhinein ein Fehler war. Dasselbe gilt für das Umlaufverhalten von unsigned (ich hätte es besser für normale Typen gehabt, UB im Überlauf zu haben und separate Typen mit Umlaufverhalten zu haben, wenn Sie das explizit brauchen)

    – Bolov

    9. Mai 2019 um 9:45 Uhr

  • @immibis siehst du, wie der Compiler transformiert x + 10 >= x; hinein mov eax, 1? Das ist gerade, weil es annehmen kann x + 10 >= x wird immer wahr sein (und ich sage es noch einmal zum letzten Mal: für gültigen Code)

    – Bolov

    10. Mai 2019 um 0:26 Uhr


  • @immibis du machst das an dieser Stelle absichtlich. Sie lenken die Diskussion vom ursprünglichen Problem weg. Meine Aussage lautet: “Der Compiler geht davon aus, dass für gültigen Code x + 10 >= x wird immer wahr sein – dies wird vom Standard vorgeschrieben – damit der Compiler den Ausdruck darauf optimieren kann true“. Das war immer meine Aussage. Das ist, was ich debattiere (und was Sie anfangs bestritten haben). Sie wollen jetzt darüber sprechen, wie der Programmierer sicherstellen muss, dass er keinen Code schreibt, der UB hat und daher ungültig ist, das ist eine andere Diskussion .

    – Bolov

    10. Mai 2019 um 0:46 Uhr


Nicht ganz ein Beispiel für Optimierung, aber eine nützliche Folge von undefiniertem Verhalten ist -ftrapv Befehlszeilenschalter von GCC/clang. Es fügt Code ein, der Ihr Programm bei einem Ganzzahlüberlauf zum Absturz bringt.

Es funktioniert nicht mit Ganzzahlen ohne Vorzeichen, in Übereinstimmung mit der Idee, dass ein Überlauf ohne Vorzeichen beabsichtigt ist.

Der Wortlaut des Standards zum Überlauf von vorzeichenbehafteten Ganzzahlen stellt sicher, dass Menschen nicht absichtlich überlaufenden Code schreiben ftrapv ist ein nützliches Werkzeug, um unbeabsichtigten Überlauf zu entdecken.

  • Ja. Das Produzieren eines falschen Werts ist ein ernsthaftes Problem, und ein korrekt aussehender Wert ist problematischer. In vielen Fällen gibt es keine Erwartungen an das Ergebnis, sodass jeder gedruckte Wert ernst genommen werden kann. Das kann sogar zu Sicherheitslücken führen.

    – Neugieriger

    13. Mai 2019 um 7:30 Uhr

  • Der Standard ist agnostisch, ob ein Konstrukt, das UB aufruft, als fehlerhaft oder nicht portierbar angesehen werden sollte. Greift ein Programm nie absichtlich auf zB a char[10][10] Bei einem inneren Index größer als neun kann es nützlich sein, eine Implementierungsfalle zu haben, wenn dies der Fall ist. Andererseits könnten einige Aufgaben bei einer Implementierung, die es dem inneren Index ermöglichen würde, auf Daten in mehreren Zeilen zuzugreifen, effizienter ausgeführt werden, als bei einer Implementierung, bei der ein Programmierer eine Schleife ersetzen müsste, die die Elemente in mehreren Zeilen durchlaufen würde, mit einem Double Schleife das …

    – Superkatze

    16. Februar um 21:05 Uhr

  • … behandelt jede Zeile separat. Während der Standard einer Implementierung erlauben würde anzunehmen, dass sie niemals etwas anderes als streng konforme Programme erhalten wird, bedeutet dies nicht, dass eine solche Annahme für die meisten Implementierungen vernünftig wäre.

    – Superkatze

    16. Februar um 21:06 Uhr

Hier ist ein tatsächlicher kleiner Benchmark, Bubble Sort. Ich habe Zeiten ohne/mit verglichen -fwrapv (was bedeutet, dass der Überlauf UB/nicht UB ist). Hier sind die Ergebnisse (Sekunden):

                   -O3     -O3 -fwrapv    -O1     -O1 -fwrapv
Machine1, clang    5.2     6.3            6.8     7.7
Machine2, clang-8  4.2     7.8            6.4     6.7
Machine2, gcc-8    6.6     7.4            6.5     6.5

Wie Sie sehen können, ist das Nicht-UB (-fwrapv)-Version ist fast immer langsamer, der größte Unterschied ist ziemlich groß, 1,85x.

Hier ist der Code. Beachten Sie, dass ich bewusst eine Implementierung gewählt habe, die für diesen Test einen größeren Unterschied erzeugen sollte.

#include <stdio.h>
#include <stdlib.h>

void bubbleSort(int *a, long n) {
        bool swapped;
        for (int i = 0; i < n-1; i++) {
                swapped = false;
                for (int j = 0; j < n-i-1; j++) {
                        if (a[j] > a[j+1]) {
                                int t = a[j];
                                a[j] = a[j+1];
                                a[j+1] = t;
                                swapped = true;
                        }
                }

                if (!swapped) break;
        }
}

int main() {
        int a[8192];

        for (int j=0; j<100; j++) {
                for (int i=0; i<8192; i++) {
                        a[i] = rand();
                }

                bubbleSort(a, 8192);
        }
}

Die Antwort liegt eigentlich in deiner Frage:

Die meisten CPUs implementieren jedoch vorzeichenbehaftete Arithmetik mit definierter Semantik

Ich kann mir keine CPU vorstellen, die Sie heute kaufen können, die keine Zweier-Kompliment-Arithmetik für vorzeichenbehaftete Ganzzahlen verwendet, aber das war nicht immer der Fall.

Die Sprache C wurde 1972 erfunden. Damals gab es noch IBM 7090 Mainframes. Nicht alle Computer waren zwei Komplimente.

Die Sprache (und das Überlaufverhalten) um das 2s-Kompliment herum zu definieren, wäre der Codegenerierung auf Maschinen abträglich gewesen, die dies nicht waren.

Darüber hinaus ermöglicht es, wie bereits gesagt, die Angabe, dass ein signierter Überlauf UB sein soll, dem Compiler, besseren Code zu erzeugen, da er Codepfade, die aus einem signierten Überlauf resultieren, abwerten kann, vorausgesetzt, dass dies niemals passieren wird.

Wenn ich richtig verstehe, dass es beabsichtigt ist, die Summe von a und b ohne Wraparound auf 0 zu klemmen….INT_MAX, kann ich mir zwei Möglichkeiten vorstellen, diese Funktion konform zu schreiben.

Zuerst der ineffiziente allgemeine Fall, der auf allen CPUs funktioniert:

int sum_max(int a, unsigned char b) {
    if (a > std::numeric_limits<int>::max() - b)
        return std::numeric_limits<int>::max();
    else
        return a + b;
}

Zweitens der überraschend effiziente 2s-Kompliment-spezifische Weg:

int sum_max2(int a, unsigned char b) {
    unsigned int buffer;
    std::memcpy(&buffer, &a, sizeof(a));
    buffer += b;
    if (buffer > std::numeric_limits<int>::max())
        buffer = std::numeric_limits<int>::max();
    std::memcpy(&a, &buffer, sizeof(a));
    return a;
}

Der resultierende Assembler ist hier zu sehen: https://godbolt.org/z/F42IXV

1390790cookie-checkGibt es einige aussagekräftige statistische Daten, um zu rechtfertigen, dass der arithmetische Überlauf von vorzeichenbehafteten Ganzzahlen undefiniert bleibt?

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

Privacy policy