在两个大整数的乘法期间捕获并计算溢出

时间:2009-11-29 12:14:59

标签: c integer bit-manipulation overflow multiplication

我正在寻找一种有效(可选的标准,优雅且易于实现)的解决方案,以便将相对较大的数字相乘,并将结果存储为一个或多个整数:

假设我有两个64位整数,如下所示:

uint64_t a = xxx, b = yyy; 

当我a * b时,如何检测操作是否导致溢出,并且在这种情况下将进位存储在某处?

请注意我不想使用任何大号库,因为我对存储号码的方式有限制。

13 个答案:

答案 0 :(得分:70)

<强> 1。检测溢出:

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

修改:修正0分区(谢谢马克!)

<强> 2。计算进位非常复杂。一种方法是将两个操作数拆分为半字,然后将long multiplication应用于半字:

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;
}

为了看到没有任何部分和本身可以溢出,我们考虑最坏的情况:

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

B = 1 << 32。然后我们

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

我相信这会奏效 - 至少它会处理Sjlver的测试用例。除此之外,它是未经测试的(甚至可能无法编译,因为我手头没有C ++编译器了。)

答案 1 :(得分:30)

这个想法是使用以下事实,这对积分操作是正确的:

a*b > c当且仅当a > c/b

/在这里是不可分割的部门。

检查正数溢出的伪代码如下:

if(a&gt; max_int64 / b)然后“溢出”,否则“ok”

要处理零和负数,您应该添加更多检查。

非负ab的C代码如下:

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

注意:

18446744073709551615 == (1<<64)-1

为了计算进位,我们可以使用方法将数字拆分成两个32位数,然后在纸上将它们相乘。我们需要拆分数字以避免溢出。

代码如下:

// 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

答案 2 :(得分:23)

虽然这个问题还有其他几个答案,但我有几个代码完全没有经过测试,到目前为止还没有人能够充分比较不同的选择。

出于这个原因,我编写并测试了几种可能的实现(最后一种实现基于OpenBSD的this code,在Reddit here上讨论过)。这是代码:

/* 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);
}

以下是结果,使用我所拥有的各种编译器和系统进行测试(在这种情况下,所有测试都是在OS X上完成的,但结果在BSD或Linux系统上应该类似):

+------------------+----------+----------+----------+----------+----------+
|                  | 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 |
+------------------+----------+----------+----------+----------+----------+

基于这些结果,我们可以得出一些结论:

  • 显然,基于分工的方法虽然简单易行,但速度很慢。
  • 在所有情况下,没有任何技术是明显的赢家。
  • 在现代编译器中,如果可以使用,那么使用大型int方法是最好的
  • 在较旧的编译器上,长乘法方法最好
  • 令人惊讶的是,GCC 4.9.0的性能回归高于GCC 4.2.1,而GCC 4.2.1的性能回归高于GCC 3.3

答案 3 :(得分:9)

在== 0:

时也适用的版本
    x = a * b;
    if (a != 0 && x / a != b) {
        // overflow handling
    }

答案 4 :(得分:6)

如果您不仅需要检测溢出而且还需要捕获进位,那么最好将数字分解为32位部分。代码是一场噩梦;以下只是草图:

#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...
}

问题不仅仅是部分产品,而是任何总和可能溢出的事实。

如果我必须真正做到这一点,我会用本地汇编语言编写扩展乘法例程。也就是说,例如,将两个64位整数相乘得到128-位结果,存储在两个64位寄存器中。所有合理的硬件都在单个本机乘法指令中提供此功能 - 它不仅可以从C中访问。

这是极少数情况下,其中最优雅且易于编程的解决方案实际上是使用汇编语言。但它肯定不便携: - (

答案 5 :(得分:3)

使用clang和gcc轻松快捷:

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

这将使用硬件支持(如果可用)进行溢出检测。通过编译器扩展,它甚至可以处理有符号整数溢出(用smul代替umul),尽管这在C ++中是未定义的行为。

答案 6 :(得分:1)

我这几天一直在处理这个问题而且我不得不说,我看到人们说最好的方式来知道是否存在溢出是分开结果的次数给我留下了深刻的印象,那就是完全没有效率和不必要的。这个功能的要点是它必须尽可能快。

溢出检测有两种选择:

1º-如果可能,创建两倍于乘数的结果变量,例如:

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)
}

您将立即知道是否存在溢出,并且代码是最快的,而无需在机器代码中编写代码。根据编译器,可以在机器代码中改进此代码。

2º-无法创建两倍于乘数变量的结果变量: 然后你应该玩if条件来确定最佳路径。继续举例:

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);
}
}

我希望这段代码可以帮助你提供一个非常有效的程序,我希望代码是清晰的,如果不是,我会放一些代码。

最好的问候。

答案 7 :(得分:0)

这是检测两个无符号整数的乘法是否溢出的技巧。

我们观察到,如果我们将N位宽的二进制数与M位宽的二进制数相乘,则乘积不会超过N + M位。

例如,如果要求我们将三位数乘以二十九位数,我们就知道不会溢出三十二位。

#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;
}

一些测试:(在64位系统上):

$ ./uflow 0x3 0x3FFFFFFFFFFFFFFF
3 * 4611686018427387903 = 13835058055282163709

$ ./uflow 0x7 0x3FFFFFFFFFFFFFFF
might be multiplication overflow
7 * 4611686018427387903 = 13835058055282163705
confirmed multiplication overflow

$ ./uflow 0x4 0x3FFFFFFFFFFFFFFF
might be multiplication overflow
4 * 4611686018427387903 = 18446744073709551612

$ ./uflow 0x5 0x3FFFFFFFFFFFFFFF
might be multiplication overflow
5 * 4611686018427387903 = 4611686018427387899
confirmed multiplication overflow

might_be_mul_oflow中的步骤几乎肯定比仅进行除法测试要慢,至少在桌面工作站,服务器和移动设备中使用的主流处理器上是这样。在没有良好分工支持的芯片上,它可能很有用。

我觉得还有另一种方法可以做到这种早期拒绝测试。

  1. 我们从一对数字arngbrng开始,这些数字已初始化为0x7FFF...FFFF1

  2. 如果a <= arngb <= brng我们可以断定没有溢出。

  3. 否则,我们将arng向右移,向左移brng,向brng添加一位,使它们为0x3FFF...FFFF并且3

  4. 如果arng为零,则完成;否则在2处重复。

  5. 该功能现在看起来像:

    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;
      }
    }
    

答案 8 :(得分:0)

如果你只是想检测溢出,那么如何转换为double,进行乘法以及if

| X | &LT; 2 ^ 53,转换为int64

| X | &LT; 2 ^ 63,使用int64进行乘法

否则会产生你想要的任何错误?

这似乎有效:

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;
}

答案 9 :(得分:0)

GNU可移植性库(Gnulib)包含模块intprops,该模块具有可有效测试算术运算是否溢出的宏。

例如,如果发生乘法溢出,则 const { history } = props.props history.push('/next'); Component.propTypes = { props: { history: PropTypes.object, }, } Component.defaultProps = { props: { history: PropTypes.object, }, } 将产生INT_MULTIPLY_OVERFLOW (a, b)

答案 10 :(得分:0)

有一个尚未提及的简单(通常是非常快速的解决方案)。该解决方案基于以下事实:对于n + m位或更高的乘积宽度,n位乘以m位乘法不会溢出。

因此,基本上,您需要检查的是,两个因素中前导零位的总和是否足以防止溢出。我对解决方案的真正喜欢是关于位乘法的数学方面。设两个操作数和结果均为n位,则很容易证明任何小于结果位宽度n 的前导零之和(感谢评论编辑)给您溢出,或者如果前导零和足够大(至少为n),则不会溢出。如果仅采用前导零之和减去n -1 的因数的最小乘积,则肯定会发生溢出。 (不过请注意,如果前导零和等于n-1是不确定的。您需要稍微扩展一下才能识别出前导零和== n-1的特殊情况。)

之所以采用这种方法比上面提出的除法方法效率更高的原因是基于以下事实:许多流行的处理器都支持使用本机指令对前导零进行计数,这比对零进行分支检查,除法然后再次比较分支。除法运算通常比计算前导零(如果作为机器指令支持,则在一个周期内)进行计算(计算在ARM Cortex-M上需要10个以上的时钟周期)要花费更长的时间,甚至不需要对它们进行计数。由您自己编程,甚至不需要使用内联汇编器!

诀窍是使用内建函数/内部函数。在GCC中,它看起来是这样的:

/**@fn static inline _Bool chk_mul_ov(uint32_t f1, uint32_t f2)
 * @return one, if a 32-Bit-overflow occurs when unsigned-unsigned-multipliying f1 with f2 otherwise zero. */
static inline _Bool chk_mul_ov(uint32_t f1, uint32_t f2) {
    int lzsum = builtin_clz(f1) + builtin_clz(f2); //leading zero sum
    return
        lzsum < sizeof(f1)*8-1 || ( //if too small, overflow guaranteed
            lzsum == sizeof(f1)*8-1 && //if special case, do further check
            (int32_t)((f1 >> 1)*f2 + (f1 & 1)*(f2 >> 1)) < 0 //check product rightshifted by one
    );
}
...
    if (chk_mul_ov(f1, f2)) {
        //error handling
    }
...

这只是一个仅适用于32位unsigned-unsigned-multiplication的示例。甚至不需要多个位移位(因为某些微控制器仅实现一位位移)。但是,如果没有count-leading-zeros指令,而只有一个乘法指令,那可能不只是将所有位相乘更好。

其他编译器有自己的方式为CLZ操作指定内部函数。 我认为,在许多情况下,此解决方案可能与计算128位乘积并检查较高的一半一样快。许多处理器甚至可能甚至不提供UUMULL,SUMULL或SSMULL类型的指令或64位寄存器,这意味着它们将需要4个寄存器用于128位。因此,计数前导零方法甚至可能比使用高度优化的128位乘法检查64位溢出更好(在最坏的情况下)。乘法需要线性开销,而计数位仅需要线性开销。我没有在实践中尝试我的想法,但希望它能解决问题。

答案 11 :(得分:0)

当您使用例如64 位变量,使用 nsb(var) = { 64 - clz(var); }.

clz(var) = 计算 var 中的前导零,GCC 和 Clang 的内置命令,或者可能与 CPU 的内联汇编一起使用。

现在使用 nsb(a * b) <= nsb(a) + nsb(b) 来检查溢出。当较小时,它总是小 1。

Ref GCC:内置函数:int __builtin_clz (unsigned int x) 返回 x 中前导 0 位的数量,从最高有效位位置开始。如果 x 为 0,则结​​果未定义。

答案 12 :(得分:0)

我今天正在思考这个问题,偶然发现了这个问题,我的想法使我得出了这个结果。 TLDR,虽然我觉得它“优雅”,因为它只使用了几行代码(很容易成为一行代码),并且有一些温和的数学,从概念上简化为相对简单的东西,这主要是“有趣的”,我没有没有测试过。

如果您将无符号整数视为基数为 2^n 的单个数字,其中 n 是整数中的位数,那么您可以将这些数字映射到单位圆周围的弧度,例如

radians(x) = x * (2 * pi * rad / 2^n)

当整数溢出时,相当于绕了一圈。所以计算进位相当于计算乘法环绕圆的次数。为了计算我们环绕圆的次数,我们将弧度(x)除以 2pi 弧度。例如

wrap(x) = radians(x) / (2*pi*rad)
        = (x * (2*pi*rad / 2^n)) / (2*pi*rad / 1)
        = (x * (2*pi*rad / 2^n)) * (1 / 2*pi*rad)
        = x * 1 / 2^n
        = x / 2^n

简化为

wrap(x) = x / 2^n

这是有道理的。一个数(例如基数为 10 的 15)环绕的次数是 15 / 10 = 1.5,即一次半。但是,我们这里不能使用 2 位数字(假设我们仅限于单个 2^64 位数字)。

假设我们有一个* b,基数为R,我们可以计算进位

Consider that: wrap(a * b) = a * wrap(b)
wrap(a * b) = (a * b) / R
a * wrap(b) = a * (b / R)
a * (b / R) = (a * b) / R

carry = floor(a * wrap(b))

a = 9b = 5 为例,它们是 45 的因数(即 9 * 5 = 45)。

wrap(5) = 5 / 10 = 0.5
a * wrap(5) = 9 * 0.5 = 4.5
carry = floor(9 * wrap(5)) = floor(4.5) = 4

请注意,如果进位为 0,那么我们就不会发生溢出,例如如果 a = 2, b=2

在 C/C++ 中(如果编译器和架构支持的话)我们必须使用 long double。

因此我们有:

long double wrap = b / 18446744073709551616.0L; // this is b / 2^64
unsigned long carry = (unsigned long)(a * wrap); // floor(a * wrap(b))
bool overflow = carry > 0;
unsigned long c = a * b;

c 是较低的有效“数字”,即以 10 为底的 9 * 9 = 81carry = 8c = 1

理论上这对我来说很有趣,所以我想我会分享它,但一个主要的警告是计算机中的浮点精度。使用 long double,当我们计算 wrap 变量时,某些数字可能会出现舍入错误,具体取决于您的编译器/架构对 long double 使用的有效数字的数量,我相信应该还有 20 多位。此结果的另一个问题是,仅通过使用浮点数和除法,它的性能可能不如其他一些解决方案。