Überlauf während der Multiplikation zweier großer Ganzzahlen abfangen und berechnen

Lesezeit: 13 Minuten

Bens Benutzeravatar
Ben

Ich suche nach einer effizienten (optional standardmäßigen, eleganten und einfach zu implementierenden) Lösung, um relativ große Zahlen zu multiplizieren und das Ergebnis in einer oder mehreren ganzen Zahlen zu speichern:

Nehmen wir an, ich habe zwei 64-Bit-Ganzzahlen, die wie folgt deklariert sind:

uint64_t a = xxx, b = yyy; 

Wenn ich es tue a * bwie kann ich feststellen, ob die Operation zu einem Überlauf führt, und in diesem Fall den Übertrag irgendwo speichern?

Bitte beachte, dass Ich möchte keine umfangreiche Bibliothek verwenden da ich Einschränkungen bei der Art und Weise habe, wie ich die Nummern speichere.

  • Ausschließlich aus C-Standardtext kann die unsigned Integer-Multiplikation nicht überlaufen, aber sie kann umlaufen. Das Verhalten des Signed-Integer-Überlaufs ist undefiniert. Es gibt Antworten auf diese Frage, die strikt davon ausgehen, dass die Operanden vorzeichenlos sind und nicht als solche für vorzeichenbehaftete Ganzzahlen verwendet werden können.

    – Antti Haapala – Слава Україні

    8. Juni 2019 um 15:38 Uhr

  • Die vorzeichenbehaftete Ganzzahl wickelt sich auch um, soweit ich weiß

    – H-005

    21. Juli 2020 um 8:58 Uhr

  • @H-005: Die Assembleranweisungen, die der Compiler für die Multiplikation von vorzeichenbehafteten Ganzzahlen generiert, können umlaufen, aber der Optimierer in Ihrem Compiler geht mit ziemlicher Sicherheit davon aus, dass kein Überlauf auftritt, und optimiert entsprechend. Dies bedeutet, dass Ihr Programm alles tun kann, wenn Sie auf einen signierten Überlauf stoßen, da ein signierter Überlauf ein undefiniertes Verhalten ist.

    – David Stein

    12. Juni 2021 um 19:20 Uhr


Benutzeravatar von meriton
Verdienst

1. Erkennen des Überlaufs:

x = a * b;
if (a != 0 && x / a != b) {
    // overflow handling
}

Edit: Division durch korrigiert 0 (Danke Markus!)

2. Berechnen des Übertrags ist ziemlich involviert. Ein Ansatz besteht darin, beide Operanden in Halbwörter aufzuteilen und dann anzuwenden lange Multiplikation zu den Halbwörtern:

uint64_t hi(uint64_t x) {
    return x >> 32;
}

uint64_t lo(uint64_t x) {
    return ((1ULL << 32) - 1) & x;
}

void multiply(uint64_t a, uint64_t b) {
    // actually uint32_t would do, but the casting is annoying
    uint64_t s0, s1, s2, s3; 
    
    uint64_t x = lo(a) * lo(b);
    s0 = lo(x);
    
    x = hi(a) * lo(b) + hi(x);
    s1 = lo(x);
    s2 = hi(x);
    
    x = s1 + lo(a) * hi(b);
    s1 = lo(x);
    
    x = s2 + hi(a) * hi(b) + hi(x);
    s2 = lo(x);
    s3 = hi(x);
    
    uint64_t result = s1 << 32 | s0;
    uint64_t carry = s3 << 32 | s2;
}

Damit keine der Teilsummen selbst überlaufen kann, betrachten wir den ungünstigsten Fall:

        x = s2 + hi(a) * hi(b) + hi(x)

Lassen B = 1 << 32. Wir haben dann

            x <= (B - 1) + (B - 1)(B - 1) + (B - 1)
              <= B*B - 1
               < B*B

Ich glaube, das wird funktionieren – zumindest behandelt es Sjlvers Testfall. Abgesehen davon ist es ungetestet (und wird möglicherweise nicht einmal kompiliert, da ich keinen C++-Compiler mehr zur Hand habe).

  • Der Kommentar von caf ist falsch. Der C99-Standard schreibt vor, dass „eine Berechnung mit vorzeichenlosen Operanden niemals überlaufen kann, da ein Ergebnis, das nicht durch den resultierenden vorzeichenlosen Integertyp dargestellt werden kann, modulo um die Zahl reduziert wird, die um eins größer ist als der größte Wert, der durch den resultierenden Typ dargestellt werden kann. ” Als solche ist Meritons Lösung sowohl in der Theorie als auch in der Praxis gültig.

    – Jens Ayton

    30. November 2009 um 7:34 Uhr

  • Bist du dir absolut sicher, dass das richtig ist? Betrachten Sie a = 7 und b = 613612691. Diese Berechnung läuft über (für 32 Bit), aber der Übertrag (Ihrer Meinung nach) ist Null. Es tut mir leid, nach mehr als drei Jahren darauf zurückzukommen … aber es wäre traurig, wenn StackOverflow eine falsch akzeptierte Antwort hätte.

    – Sjlver

    18. Februar 2013 um 11:27 Uhr


  • Fest. (Ich programmiere viel Java, wobei >> der rechte Shift-Operator mit Vorzeichenerweiterung und >>> der rechte Shift-Operator ohne Vorzeichenerweiterung ist. In C gibt es nur >>, und die Vorzeichenerweiterung hängt vermutlich von der Signiertheit ab der ganzzahligen Eingänge).

    – Verdienst

    3. August 2015 um 16:13 Uhr

  • Annehmen, dass a * b überläuft. Dann, a * b ≤ ab - 2^64 und deshalb a * b / a ≤ ab/a - (2^64/a) < b.

    – Verdienst

    10. April 2017 um 0:43 Uhr


  • @JensAyton: Sie haben also herausgefunden, was der Standard über unsigned Overflow sagt. Aber der Kommentar von caf betraf den signierten Überlauf.

    – Sebastian Mach

    1. April 2019 um 11:26 Uhr

Benutzeravatar von sergtk
sergtk

Die Idee ist, die folgende Tatsache zu verwenden, die für den integralen Betrieb gilt:

a*b > c dann und nur dann, wenn a > c/b

/ ist hier ganzzahlige Division.

Der Pseudocode zum Prüfen auf Überlauf für positive Zahlen folgt:

if (a > max_int64 / b) then “overflow” else “ok”.

Um Nullen und negative Zahlen zu behandeln, sollten Sie weitere Prüfungen hinzufügen.

C-Code für nicht negativ a und b folgt:

if (b > 0 && a > 18446744073709551615 / b) {
     // overflow handling
}; else {
    c = a * b;
}

Notiz:

18446744073709551615 == (1<<64)-1

Um den Übertrag zu berechnen, können wir den Ansatz verwenden, um die Zahl in zwei 32-stellige Zahlen aufzuteilen und sie zu multiplizieren, während wir dies auf dem Papier tun. Wir müssen Zahlen aufteilen, um einen Überlauf zu vermeiden.

Code folgt:

// split input numbers into 32-bit digits
uint64_t a0 = a & ((1LL<<32)-1);
uint64_t a1 = a >> 32;
uint64_t b0 = b & ((1LL<<32)-1);
uint64_t b1 = b >> 32;


// The following 3 lines of code is to calculate the carry of d1
// (d1 - 32-bit second digit of result, and it can be calculated as d1=d11+d12),
// but to avoid overflow.
// Actually rewriting the following 2 lines:
// uint64_t d1 = (a0 * b0 >> 32) + a1 * b0 + a0 * b1;
// uint64_t c1 = d1 >> 32;
uint64_t d11 = a1 * b0 + (a0 * b0 >> 32); 
uint64_t d12 = a0 * b1;
uint64_t c1 = (d11 > 18446744073709551615 - d12) ? 1 : 0;

uint64_t d2 = a1 * b1 + c1;
uint64_t carry = d2; // needed carry stored here

  • Wenn Sie den Übertrag sowieso brauchen (oder ihn oft genug brauchen), können Sie ihn auch einfach berechnen und auf Nicht-Null prüfen. Viele 32-Bit-Systeme implementieren eine 64-Bit-Multiplikation ohnehin als leicht abgespeckte Version davon, sodass sie mit ein paar Kurzstopp-Prüfungen möglicherweise nur ein wenig langsamer ist als die direkte Multiplikation.

    – BKS

    12. Juli 2010 um 0:41 Uhr


  • c1 = (d11 > 18446744073709551615 - d12) ? 1 : 0; erzeugt den falschen Carry. Brauchen Sie so etwas wie c1 = d11 + d12; if (c1 < d11) c1 = c1 >> 32 + 0x100000000u; else c1 >>= 32;

    – chux – Wiedereinsetzung von Monica

    19. Dezember 2014 um 17:48 Uhr

  • @chux-ReinstateMonica d11 > 18446744073709551615 - d12 ist genau das Überlaufen zu vermeiden. Wenn ein Überlauf auftreten kann, bedeutet Überlauf, dass der Übertrag 1 ist. Wenn Sie einen direkten Vergleich versuchen (d11+d12 > 18446744073709551615), das ist es, was zum Überlaufen führen kann, und wir wollen es nicht. Und Überlauf bedeutet hier eigentlich Übertrag. Es wäre gut, wenn Sie genaue Werte angeben, wenn der Übertrag falsch ist

    – sergtk

    8. Februar 2020 um 16:54 Uhr


  • @sergtk Ich werde mein 5-jähriges Anliegen später noch einmal durchgehen. Hinweis: 18446744073709551615 ist problematisch, da es wahrscheinlich nicht in der ist long long Reichweite – wie für portablen Code für a benötigt Dezimal konstant – ohne u. 18446744073709551615u wäre besser. 18446744073709551615 ist auch eine nackte magische Zahl. UINT64_MAX würde beide vorherigen Probleme lösen und die Absicht des Codes besser vermitteln.

    – chux – Wiedereinsetzung von Monica

    8. Februar 2020 um 20:46 Uhr

  • @chux-ReinstateMonica Da die Leute die Antwort nach langer Zeit weiterhin positiv bewerten, habe ich beschlossen, die Kommentare zu überprüfen. Das Einzige, worüber ich mir Sorgen gemacht habe, ist das Produzieren falscher Ergebnisse, zB bei Grenzfällen oder ähnlichem. Zu deinem Kommentar. Da 18446744073709551615 nicht in „Long Long Range“ ist, wird es in „Unsigned Long Long“ sein. Ich könnte ‘u’ und UINT64_MAX hinzufügen, es sieht im Endcode gut aus, aber bei dieser Antwort geht es nicht um das Kopieren / Einfügen von Codestücken. 18446744073709551615 wird in der Antwort beschrieben, in dieser Antwort geht es nicht um den Codestil oder ähnliches.

    – sergtk

    9. Februar 2020 um 13:14 Uhr

Benutzeravatar von Charphacy
Charphatie

Obwohl es mehrere andere Antworten auf diese Frage gegeben hat, haben einige von ihnen Code, der völlig ungetestet ist, und bisher hat niemand die verschiedenen möglichen Optionen angemessen verglichen.

Aus diesem Grund habe ich mehrere mögliche Implementierungen geschrieben und getestet (die letzte basiert auf dieser Code von OpenBSD, diskutiert auf Reddit hier). Hier ist der Code:

/* Multiply with overflow checking, emulating clang's builtin function
 *
 *     __builtin_umull_overflow
 *
 * This code benchmarks five possible schemes for doing so.
 */

#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <limits.h>

#ifndef BOOL
    #define BOOL int
#endif

// Option 1, check for overflow a wider type
//    - Often fastest and the least code, especially on modern compilers
//    - When long is a 64-bit int, requires compiler support for 128-bits
//      ints (requires GCC >= 3.0 or Clang)

#if LONG_BIT > 32
    typedef __uint128_t long_overflow_t ;
#else
    typedef uint64_t long_overflow_t;
#endif

BOOL 
umull_overflow1(unsigned long lhs, unsigned long rhs, unsigned long* result)
{
        long_overflow_t prod = (long_overflow_t)lhs * (long_overflow_t)rhs;
        *result = (unsigned long) prod;
        return (prod >> LONG_BIT) != 0;
}

// Option 2, perform long multiplication using a smaller type
//    - Sometimes the fastest (e.g., when mulitply on longs is a library
//      call).
//    - Performs at most three multiplies, and sometimes only performs one.
//    - Highly portable code; works no matter how many bits unsigned long is

BOOL 
umull_overflow2(unsigned long lhs, unsigned long rhs, unsigned long* result)
{
        const unsigned long HALFSIZE_MAX = (1ul << LONG_BIT/2) - 1ul;
        unsigned long lhs_high = lhs >> LONG_BIT/2;
        unsigned long lhs_low  = lhs & HALFSIZE_MAX;
        unsigned long rhs_high = rhs >> LONG_BIT/2;
        unsigned long rhs_low  = rhs & HALFSIZE_MAX;

        unsigned long bot_bits = lhs_low * rhs_low;
        if (!(lhs_high || rhs_high)) {
            *result = bot_bits;
            return 0; 
        }
        BOOL overflowed = lhs_high && rhs_high;
        unsigned long mid_bits1 = lhs_low * rhs_high;
        unsigned long mid_bits2 = lhs_high * rhs_low;

        *result = bot_bits + ((mid_bits1+mid_bits2) << LONG_BIT/2);
        return overflowed || *result < bot_bits
            || (mid_bits1 >> LONG_BIT/2) != 0
            || (mid_bits2 >> LONG_BIT/2) != 0;
}

// Option 3, perform long multiplication using a smaller type (this code is
// very similar to option 2, but calculates overflow using a different but
// equivalent method).
//    - Sometimes the fastest (e.g., when mulitply on longs is a library
//      call; clang likes this code).
//    - Performs at most three multiplies, and sometimes only performs one.
//    - Highly portable code; works no matter how many bits unsigned long is

BOOL 
umull_overflow3(unsigned long lhs, unsigned long rhs, unsigned long* result)
{
        const unsigned long HALFSIZE_MAX = (1ul << LONG_BIT/2) - 1ul;
        unsigned long lhs_high = lhs >> LONG_BIT/2;
        unsigned long lhs_low  = lhs & HALFSIZE_MAX;
        unsigned long rhs_high = rhs >> LONG_BIT/2;
        unsigned long rhs_low  = rhs & HALFSIZE_MAX;

        unsigned long lowbits = lhs_low * rhs_low;
        if (!(lhs_high || rhs_high)) {
            *result = lowbits;
            return 0; 
        }
        BOOL overflowed = lhs_high && rhs_high;
        unsigned long midbits1 = lhs_low * rhs_high;
        unsigned long midbits2 = lhs_high * rhs_low;
        unsigned long midbits  = midbits1 + midbits2;
        overflowed = overflowed || midbits < midbits1 || midbits > HALFSIZE_MAX;
        unsigned long product = lowbits + (midbits << LONG_BIT/2);
        overflowed = overflowed || product < lowbits;

        *result = product;
        return overflowed;
}

// Option 4, checks for overflow using division
//    - Checks for overflow using division
//    - Division is slow, especially if it is a library call

BOOL
umull_overflow4(unsigned long lhs, unsigned long rhs, unsigned long* result)
{
        *result = lhs * rhs;
        return rhs > 0 && (SIZE_MAX / rhs) < lhs;
}

// Option 5, checks for overflow using division
//    - Checks for overflow using division
//    - Avoids division when the numbers are "small enough" to trivially
//      rule out overflow
//    - Division is slow, especially if it is a library call

BOOL
umull_overflow5(unsigned long lhs, unsigned long rhs, unsigned long* result)
{
        const unsigned long MUL_NO_OVERFLOW = (1ul << LONG_BIT/2) - 1ul;
        *result = lhs * rhs;
        return (lhs >= MUL_NO_OVERFLOW || rhs >= MUL_NO_OVERFLOW) &&
            rhs > 0 && SIZE_MAX / rhs < lhs;
}

#ifndef umull_overflow
    #define umull_overflow2
#endif

/*
 * This benchmark code performs a multiply at all bit sizes, 
 * essentially assuming that sizes are logarithmically distributed.
 */

int main()
{
        unsigned long i, j, k;
        int count = 0;
        unsigned long mult;
        unsigned long total = 0;

        for (k = 0; k < 0x40000000 / LONG_BIT / LONG_BIT; ++k)
                for (i = 0; i != LONG_MAX; i = i*2+1)
                        for (j = 0; j != LONG_MAX; j = j*2+1) {
                                count += umull_overflow(i+k, j+k, &mult);
                                total += mult;
                        }
        printf("%d overflows (total %lu)\n", count, total);
}

Hier sind die Ergebnisse, Tests mit verschiedenen Compilern und Systemen, die ich habe (in diesem Fall wurden alle Tests unter OS X durchgeführt, aber die Ergebnisse sollten auf BSD- oder Linux-Systemen ähnlich sein):

+------------------+----------+----------+----------+----------+----------+
|                  | Option 1 | Option 2 | Option 3 | Option 4 | Option 5 |
|                  |  BigInt  | LngMult1 | LngMult2 |   Div    |  OptDiv  |
+------------------+----------+----------+----------+----------+----------+
| Clang 3.5 i386   |    1.610 |    3.217 |    3.129 |    4.405 |    4.398 |
| GCC 4.9.0 i386   |    1.488 |    3.469 |    5.853 |    4.704 |    4.712 |
| GCC 4.2.1 i386   |    2.842 |    4.022 |    3.629 |    4.160 |    4.696 |
| GCC 4.2.1 PPC32  |    8.227 |    7.756 |    7.242 |   20.632 |   20.481 |
| GCC 3.3   PPC32  |    5.684 |    9.804 |   11.525 |   21.734 |   22.517 |
+------------------+----------+----------+----------+----------+----------+
| Clang 3.5 x86_64 |    1.584 |    2.472 |    2.449 |    9.246 |    7.280 |
| GCC 4.9 x86_64   |    1.414 |    2.623 |    4.327 |    9.047 |    7.538 |
| GCC 4.2.1 x86_64 |    2.143 |    2.618 |    2.750 |    9.510 |    7.389 |
| GCC 4.2.1 PPC64  |   13.178 |    8.994 |    8.567 |   37.504 |   29.851 |
+------------------+----------+----------+----------+----------+----------+

Basierend auf diesen Ergebnissen können wir einige Schlussfolgerungen ziehen:

  • Der divisionsbasierte Ansatz ist eindeutig langsam, obwohl er einfach und übertragbar ist.
  • Keine Technik ist in allen Fällen ein klarer Gewinner.
  • Auf modernen Compilern ist der use-a-larger-int-Ansatz am besten, wenn Sie ihn verwenden können
  • Bei älteren Compilern ist der Long-Multiplication-Ansatz am besten
  • Überraschenderweise weist GCC 4.9.0 Leistungsrückgänge gegenüber GCC 4.2.1 und GCC 4.2.1 Leistungsrückgänge gegenüber GCC 3.3 auf

Eine Version, die auch funktioniert, wenn a == 0:

    x = a * b;
    if (a != 0 && x / a != b) {
        // overflow handling
    }

Einfach und schnell mit clang und gcc:

unsigned long long t a, b, result;
if (__builtin_umulll_overflow(a, b, &result)) {
    // overflow!!
}

Dadurch wird Hardwareunterstützung für die Überlauferkennung verwendet, sofern verfügbar. Da es sich um Compiler-Erweiterungen handelt, kann es sogar einen Überlauf von vorzeichenbehafteten Ganzzahlen verarbeiten (umul durch smul ersetzen), obwohl dies ein undefiniertes Verhalten in C++ ist.

Wenn Sie nicht nur den Überlauf erkennen, sondern auch den Übertrag erfassen müssen, zerlegen Sie Ihre Zahlen am besten in 32-Bit-Teile. Der Code ist ein Alptraum; Folgendes ist nur eine Skizze:

#include <stdint.h>

uint64_t mul(uint64_t a, uint64_t b) {
  uint32_t ah = a >> 32;
  uint32_t al = a;  // truncates: now a = al + 2**32 * ah
  uint32_t bh = b >> 32;
  uint32_t bl = b;  // truncates: now b = bl + 2**32 * bh
  // a * b = 2**64 * ah * bh + 2**32 * (ah * bl + bh * al) + al * bl
  uint64_t partial = (uint64_t) al * (uint64_t) bl;
  uint64_t mid1    = (uint64_t) ah * (uint64_t) bl;
  uint64_t mid2    = (uint64_t) al * (uint64_t) bh;
  uint64_t carry   = (uint64_t) ah * (uint64_t) bh;
  // add high parts of mid1 and mid2 to carry
  // add low parts of mid1 and mid2 to partial, carrying
  //    any carry bits into carry...
}

Das Problem sind nicht nur die Teilprodukte, sondern die Tatsache, dass jede der Summen überlaufen kann.

Wenn ich das wirklich tun müsste, würde ich eine Extended-Multiply-Routine in der lokalen Assemblersprache schreiben. Das heißt, multiplizieren Sie beispielsweise zwei 64-Bit-Ganzzahlen, um ein 128-Bit-Ergebnis zu erhalten, das in zwei 64-Bit-Registern gespeichert wird. Jede vernünftige Hardware bietet diese Funktionalität in einer einzigen nativen Multiplizieranweisung – sie ist nicht nur von C aus zugänglich.

Dies ist einer der seltenen Fälle, in denen die eleganteste und am einfachsten zu programmierende Lösung tatsächlich die Verwendung der Assemblersprache ist. Aber es ist sicherlich nicht tragbar 🙁

Benutzeravatar von Marc
Marc

Die GNU Portability Library (Gnulib) enthält ein Modul intpropsdas über Makros verfügt, die effizient testen, ob arithmetische Operationen überlaufen würden.

Wenn zum Beispiel ein Überlauf bei der Multiplikation auftreten würde, INT_MULTIPLY_OVERFLOW (a, b) ergäbe 1.

1419360cookie-checkÜberlauf während der Multiplikation zweier großer Ganzzahlen abfangen und berechnen

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

Privacy policy