【问题标题】:Get overflow from arithmetic operations从算术运算中获取溢出
【发布时间】:2020-02-23 00:03:07
【问题描述】:

在做数学运算时,如何得到溢出的部分?

例如,假设 32 位整数:

unsigned int a = 0Xffffffff;
unsigned int b = 0xffffffff;
unsigned int c = a + b;

在这种情况下,c0xfffffffe,但答案应该是 0x1fffffffe。我如何得到溢出的 1?

我怎样才能对乘法做同样的事情?我可以将两个大数相乘,只得到溢出的部分吗?

bigint 库如何管理这个?

【问题讨论】:

  • 可能是relevant
  • 用 64 位整数移位怎么样?
  • 由于两个数之和大于存储量,所以无处存放数,这就是溢出的原因。如果您可以在某处检查溢出,则根本不需要溢出。但是,您可以检测到溢出并执行其他操作,if(UINT_MAX - a < b) then handle_it()
  • unsigned long long int 是标准 C 类型,必须至少有 64 位,因此,对于您的 32 位示例,乘积可以计算为 (unsigned long long int) a * b,之后低和高 32位可以通过移位和掩码提取。
  • @Dr.-Ing.GerhardStein 问题会以 unsigned long long 出现。那是什么类型的呢?

标签: c math integer-overflow


【解决方案1】:

@Warning 不可携带@

  1. 使用https://gcc.gnu.org/onlinedocs/gcc/Integer-Overflow-Builtins.html

示例:https://godbolt.org/z/NcyNzR

#include <stdio.h> 
int main(void)
{
    unsigned int res;

    if(__builtin_uadd_overflow(0Xffffffff, 0Xffffffff, &res))
    {
        printf("Overflowed\n");
    }
    printf("Result: 0x%x\n", res);
}
  1. 使用内联汇编读取进位标志

【讨论】:

    【解决方案2】:

    假设无符号类型的操作数,你可以这样写:

    bool cf = a+b<a;
    

    bool cf = a>-1-b;
    

    无论是否存在更大的类型,这些都可以工作。

    乘法更难;如果没有更大的类型,就无法访​​问结果的上半部分。如果你有一个,你可以使用它。例如,如果您的操作数是uint32_t

    uint32_t upper = ((uint64_t)a * b) >> 32;
    uint32_t lower = a*b;
    

    否则,您将无法使用半号字体并使用长乘法。例如,uint64_t a,b;

    uint32_t al = a, ah = a>>32;
    uint32_t bl = b, bh = b>>32;
    

    然后结果的上半部分是ah*bh加上al*bhah*blal*bl的高位相加的进位。

    Bigint 库可以通过选择最多为最大整数类型宽度一半的肢体类型来避免这种痛苦。

    【讨论】:

      【解决方案3】:

      如何得到溢出的 1?

      之后以可移植的方式进行(不要忘记unsigned int 可能只有 16 位):

          uint32_t a = 0Xffffffff;
          uint32_t b = 0xffffffff;
          uint32_t c_low = a + b;
          uint32_t c_high;
          if(c_low >= a) {
              c_high = 0;
          } else {
              c_high = 1;
          }
      

      以可移植的方式预先完成(没有分支):

          uint32_t a = 0Xffffffff;
          uint32_t b = 0xffffffff;
          uint32_t c_low;
          uint32_t c_high;
      
          c_high = (a&b) >> 31;
          c_low = (a ^ (c_high<<31)) + b;
      

      我怎样才能对乘法做同样的事情?

      乘法没有进位,它有一个“上半部分”。具体来说;如果你将一个有 N 位的无符号整数与一个有 M 位的无符号整数相乘,那么结果将有 N+M 位;如果两个数字的大小相同,那么结果将是原来的两倍。

      遗憾的是,C 不支持“结果类型大于源/s 类型”,因此您需要“预先提升”源类型,例如:

          uint32_t a = 0Xffffffff;
          uint32_t b = 0xffffffff;
          uint64_t temp = (uint64_t)a * (uint64_t)b;
          uint32_t c_low = temp;
          uint32_t c_high = temp >> 32;
      

      当然,如果编译器不支持较大的类型,那么您必须将其拆分为较小的部分,例如:

          uint32_t a = 0Xffffffff;
          uint32_t b = 0xffffffff;
      
          uint32_t a_low = a & 0xFFFF;
          uint32_t a_high = a >> 16;
          uint32_t b_low = a & 0xFFFF;
          uint32_t b_high = b >> 16;
      
          uint32_t temp_0 = a_low * b_low;
          uint32_t temp_16a = a_high * b_low;
          uint32_t temp_16b = a_low * b_high;
          uint32_t temp_32 = a_high * b_high;
      
          uint32_t c_low = temp_0 + (temp16a << 16) + (temp16b << 16);
          uint32_t c_high = (temp16a >> 16) + (temp16b >> 16) + temp_32;
      

      bigint 库如何管理这个?

      主要是;他们使用内联汇编语言,因为大多数 CPU 支持指令以有效地处理更大的整数和/或因为您可以直接访问进位标志。例如;适用于 80x86; CPU有adc/sbbshld/shrdmul(双倍宽度结果)/div(双倍宽度分子);加上可能的扩展名(adcxadox)。

      在 32 位 80x86 汇编语言中,添加可能如下所示:

          xor edx,0
          add eax,ebx      ;c_low = a + b
          adc edx,0        ;c_high = carry
      

      ..和乘法可能看起来像:

          mul ebx          ;edx:eax = a * b
      

      【讨论】: