it-swarm.com.de

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

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

Angenommen, ich habe zwei 64-Bit-Ganzzahlen wie folgt deklariert:

uint64_t a = xxx, b = yyy; 

Wie kann ich bei a * b feststellen, ob die Operation zu einem Überlauf führt, und in diesem Fall den Carry irgendwo speichern?

Bitte beachten Sie, dass ich keine große Zahlbibliothek verwenden möchte , da ich Einschränkungen bezüglich der Art und Weise habe, wie ich die Zahlen speichere.

55
Ben

1. Überlauf erkennen:

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

Edit: Fixierte Unterteilung von 0 (Danke Mark!)

2. Die Berechnung des Carry ist ziemlich kompliziert. Ein Ansatz besteht darin, beide Operanden in Halbwörter aufzuteilen und dann long multiplication auf die Halbwörter anzuwenden:

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

uint64_t lo(uint64_t x) {
    return ((1L << 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;
}

Um zu sehen, dass keine der Teilsummen selbst überlaufen kann, betrachten wir den schlimmsten Fall:

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

Lassen Sie 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 geht es um den Testfall von Sjlver. Abgesehen davon ist es nicht getestet (und kompiliert möglicherweise nicht einmal, da ich keinen C++ - Compiler mehr zur Hand habe).

68
meriton

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

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

/ ist hier ein integraler Bereich.

Der Pseudocode zum Überprüfen auf Überlauf auf positive Zahlen folgt:

if (a> max_int64/b) dann "Überlauf" sonst "ok" .

Um mit Nullen und negativen Zahlen umgehen zu können, sollten Sie weitere Prüfungen hinzufügen.

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

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

Hinweis: 

18446744073709551615 == (1<<64)-1

Um den Carry zu berechnen, können wir die Methode verwenden, um die Zahl in zwei 32-stellige Zahlen aufzuteilen und sie auf dem Papier zu multiplizieren. Wir müssen die Zahlen teilen, um Ü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
30
sergtk

Obwohl es mehrere andere Antworten auf diese Frage gab, habe ich einige von ihnen völlig ungeprüften Code, und bisher hat niemand die verschiedenen möglichen Optionen ausreichend verglichen.

Aus diesem Grund habe ich mehrere mögliche Implementierungen geschrieben und getestet (die letzte basiert auf diesem Code von OpenBSD, 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 abteilungsbasierte Ansatz ist zwar einfach und portabel, ist jedoch langsam.
  • Keine Technik ist in allen Fällen ein klarer Gewinner.
  • Bei modernen Compilern empfiehlt sich der Gebrauch eines größeren Inters, wenn Sie ihn verwenden können
  • Bei älteren Compilern ist der Ansatz der langen Multiplikation am besten
  • Überraschenderweise hat GCC 4.9.0 einen Leistungsrückgang gegenüber GCC 4.2.1 und GCC 4.2.1 einen Leistungsrückgang gegenüber GCC 3.3
23
Charphacy

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

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

Wenn Sie nicht nur Überlauf erkennen, sondern auch den Carry erfassen möchten, sollten Sie Ihre Zahlen am besten in 32-Bit-Teile unterteilen. Der Code ist ein Albtraum; Was folgt, 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 eine der Summen überlaufen kann.

Wenn ich das wirklich machen müsste, würde ich eine erweiterte Multiplikationsroutine in der lokalen Assembly-Sprache schreiben. Das heißt zum Beispiel, multiplizieren Sie zwei 64-Bit-Ganzzahlen, um ein 128-Bit-Ergebnis zu erhalten, das in zwei 64-Bit-Registern gespeichert wird. Alle vernünftige Hardware bietet diese Funktionalität in einer einzigen systemeigenen Multiplikationsanweisung - sie ist nicht nur von C aus zugänglich.

Dies ist einer der seltenen Fälle, in denen die eleganteste und einfachste Lösung die Verwendung der Assemblersprache ist. Aber es ist sicherlich nicht tragbar :-(

6
Norman Ramsey

Der beste Weg, um dieses Problem zu lösen, ist möglicherweise eine Funktion, die zwei UInt64-Werte multipliziert und ein Paar UInt64-Werte, einen oberen Teil und einen unteren Teil des UInt128-Ergebnisses ergibt. Hier ist die Lösung, einschließlich einer Funktion, die das Ergebnis in Hex darstellt. Ich denke, Sie bevorzugen vielleicht eine C++ - Lösung, aber ich habe eine funktionierende Swift-Lösung, die zeigt, wie das Problem gelöst werden kann:

func hex128 (_ hi: UInt64, _ lo: UInt64) -> String
{
    var s: String = String(format: "%08X", hi >> 32)
                  + String(format: "%08X", hi & 0xFFFFFFFF)
                  + String(format: "%08X", lo >> 32)
                  + String(format: "%08X", lo & 0xFFFFFFFF)
    return (s)
}

func mul64to128 (_ multiplier: UInt64, _ multiplicand : UInt64)
             -> (result_hi: UInt64, result_lo: UInt64)
{
    let x: UInt64 = multiplier
    let x_lo: UInt64 = (x & 0xffffffff)
    let x_hi: UInt64 = x >> 32

    let y: UInt64 = multiplicand
    let y_lo: UInt64 = (y & 0xffffffff)
    let y_hi: UInt64 = y >> 32

    let mul_lo: UInt64 = (x_lo * y_lo)
    let mul_hi: UInt64 = (x_hi * y_lo) + (mul_lo >> 32)
    let mul_carry: UInt64 = (x_lo * y_hi) + (mul_hi & 0xffffffff)
    let result_hi: UInt64 = (x_hi * y_hi) + (mul_hi >> 32) + (mul_carry >> 32)
    let result_lo: UInt64 = (mul_carry << 32) + (mul_lo & 0xffffffff)

    return (result_hi, result_lo)
}

Hier ist ein Beispiel, um zu überprüfen, ob die Funktion funktioniert:

var c: UInt64 = 0
var d: UInt64 = 0

(c, d) = mul64to128(0x1234567890123456, 0x9876543210987654)
// 0AD77D742CE3C72E45FD10D81D28D038 is the result of the above example
print(hex128(c, d))

(c, d) = mul64to128(0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF)
// FFFFFFFFFFFFFFFE0000000000000001 is the result of the above example
print(hex128(c, d))
1
j.s.com

Ich arbeite heute mit diesem Problem, und ich muss sagen, dass es mich beeindruckt hat, wie oft Leute gesehen haben, wie der beste Weg zu wissen ist, ob es einen Überlauf gab, das Ergebnis zu teilen, das ist völlig ineffizient und nicht notwendig. Der Punkt für diese Funktion ist, dass sie so schnell wie möglich sein muss. 

Es gibt zwei Optionen für die Überlauferkennung:

1º- Wenn möglich, erstellen Sie die Ergebnisvariable doppelt so groß wie die Multiplikatoren, zum Beispiel:

struct INT32struct {INT16 high, low;};
typedef union
{
  struct INT32struct s;
  INT32 ll;
} INT32union;

INT16 mulFunction(INT16 a, INT16 b)
{
  INT32union result.ll = a * b; //32Bits result
  if(result.s.high > 0) 
      Overflow();
  return (result.s.low)
}

Sie werden sofort wissen, ob ein Überlauf aufgetreten ist und der Code so schnell wie möglich ist, ohne ihn in Maschinencode zu schreiben. Je nach Compiler kann dieser Code im Maschinencode verbessert werden.

2º- Es ist nicht möglich, eine Ergebnisvariable zu erstellen, die doppelt so groß ist wie die Multiplikatorvariable: Dann sollten Sie mit if-Bedingungen spielen, um den besten Pfad zu bestimmen. Fortsetzung des Beispiels:

INT32 mulFunction(INT32 a, INT32 b)
{

  INT32union s_a.ll = abs(a);
  INT32union s_b.ll = abs(b); //32Bits result
  INT32union result;
  if(s_a.s.hi > 0 && s_b.s.hi > 0)
  {
      Overflow();
  }
  else if (s_a.s.hi > 0)
  {
      INT32union res1.ll = s_a.s.hi * s_b.s.lo;
      INT32union res2.ll = s_a.s.lo * s_b.s.lo;
      if (res1.hi == 0)
      {
          result.s.lo = res1.s.lo + res2.s.hi;
          if (result.s.hi == 0)
          {
            result.s.ll = result.s.lo << 16 + res2.s.lo;
            if ((a.s.hi >> 15) ^ (b.s.hi >> 15) == 1)
            {
                result.s.ll = -result.s.ll; 
            }
            return result.s.ll
          }else
          {
             Overflow();
          }
      }else
      {
          Overflow();
      }
  }else if (s_b.s.hi > 0)
{

   //Same code changing a with b

}else 
{
    return (s_a.lo * s_b.lo);
}
}

Ich hoffe, dass dieser Code Ihnen hilft, ein ziemlich effizientes Programm zu haben, und ich hoffe, dass der Code klar ist, wenn nicht, werde ich ein paar Bemerkungen machen.

freundliche Grüße.

1
user1368116

Hier ist ein Trick, um festzustellen, ob die Multiplikation von zwei vorzeichenlosen Ganzzahlen überläuft.

Wir machen die Beobachtung, dass, wenn wir eine N-Bit breite Binärzahl mit einer M Bit breiten Binärzahl multiplizieren, das Produkt nicht mehr als N + M Bits hat.

Wenn wir beispielsweise gebeten werden, eine Drei-Bit-Zahl mit einer Neunundzwanzig-Bit-Zahl zu multiplizieren, wissen wir, dass diese keine / Zwei-Bit-Bits überläuft.

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

int might_be_mul_oflow(unsigned long a, unsigned long b)
{
  if (!a || !b)
    return 0;

  a = a | (a >> 1) | (a >> 2) | (a >> 4) | (a >> 8) | (a >> 16) | (a >> 32);
  b = b | (b >> 1) | (b >> 2) | (b >> 4) | (b >> 8) | (b >> 16) | (b >> 32);

  for (;;) {
    unsigned long na = a << 1;
    if (na <= a)
      break;
    a = na;
  }

  return (a & b) ? 1 : 0;
}

int main(int argc, char **argv)
{
  unsigned long a, b;
  char *endptr;

  if (argc < 3) {
    printf("supply two unsigned long integers in C form\n");
    return EXIT_FAILURE;
  }

  a = strtoul(argv[1], &endptr, 0);

  if (*endptr != 0) {
    printf("%s is garbage\n", argv[1]);
    return EXIT_FAILURE;
  }

  b = strtoul(argv[2], &endptr, 0);

  if (*endptr != 0) {
    printf("%s is garbage\n", argv[2]);
    return EXIT_FAILURE;
  }

  if (might_be_mul_oflow(a, b))
    printf("might be multiplication overflow\n");

  {
    unsigned long c = a * b;
    printf("%lu * %lu = %lu\n", a, b, c);
    if (a != 0 && c / a != b)
      printf("confirmed multiplication overflow\n");
  }

  return 0;
}

Einige kleine Tests: (auf einem 64-Bit-System):

...................................................................................... 

 $ ./uflow 0x4 0x3FFFFFFFFFFFFFFF 
 kann Multiplikationsüberlauf sein 
 * 4611686018427387903 = 18446744073709551612 .__ * 4611686018427387903 = 4611686018427387899 
 Multiplikationsüberlauf bestätigt

Die Schritte in might_be_mul_oflow sind fast sicher langsamer als der Divisionstest, zumindest auf Mainstream-Prozessoren, die in Desktop-Workstations, Servern und mobilen Geräten verwendet werden. Bei Chips ohne gute Teilungsunterstützung könnte es nützlich sein.


Es scheint mir, dass es einen anderen Weg gibt, diesen frühen Ablehnungstest durchzuführen.

  1. Wir beginnen mit einem Paar von Zahlen arng und brng, die mit 0x7FFF...FFFF und 1 initialisiert werden. 

  2. Wenn a <= arng und b <= brng können wir feststellen, dass es keinen Überlauf gibt.

  3. Andernfalls verschieben wir arng nach rechts und verschieben brng nach links und fügen ein Bit zu brng hinzu, sodass sie 0x3FFF...FFFF und 3 sind.

  4. Wenn arng Null ist, beenden Sie den Vorgang. ansonsten wieder bei 2.

Die Funktion sieht jetzt so aus:

int might_be_mul_oflow(unsigned long a, unsigned long b)
{
  if (!a || !b)
    return 0;

  {
    unsigned long arng = ULONG_MAX >> 1;
    unsigned long brng = 1;

    while (arng != 0) {
      if (a <= arng && b <= brng)
        return 0;
      arng >>= 1;
      brng <<= 1;
      brng |= 1;
    }

    return 1;
  }
}
0
Kaz

Wenn Sie nur Überlauf erkennen möchten, wie wäre es mit der Konvertierung in Double, der Multiplikation und wenn?

| x | <2 ^ 53, in int64 konvertieren

| x | <2 ^ 63, machen Sie die Multiplikation mit int64

sonst irgendeinen Fehler produzieren, den du willst?

Das scheint zu funktionieren:

int64_t safemult(int64_t a, int64_t b) {
  double dx;

  dx = (double)a * (double)b;

  if ( fabs(dx) < (double)9007199254740992 )
    return (int64_t)dx;

  if ( (double)INT64_MAX < fabs(dx) )
    return INT64_MAX;

  return a*b;
}
0
Gunnar Thorburn

Einfach und schnell mit clang und gcc:

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

Hierbei wird, sofern verfügbar, die Hardware-Unterstützung für die Überlauferkennung verwendet. Als Compiler-Erweiterung kann es sogar einen vorzeichenbehafteten Integer-Überlauf verarbeiten (umul durch smul ersetzen), obwohl dies in C++ undefiniertes Verhalten ist.

0
Allan Jensen