【问题标题】:detecting multiplication of uint64_t integers overflow with C用 C 检测 uint64_t 整数的乘法溢出
【发布时间】:2012-01-21 23:25:54
【问题描述】:

是否有任何有效且可移植的方法来检查 C 中使用 int64_t 或 uint64_t 操作数的乘法运算何时溢出?

例如,添加 uint64_t 我可以这样做:

if (UINT64_MAX - a < b) overflow_detected();
else sum = a + b;

但我无法得到类似的简单乘法表达式。

我想到的就是将操作数分解为高和低 uint32_t 部分,并在检查溢出的同时执行这些部分的乘法,这确实很丑陋,也可能效率低下。

更新 1:添加了一些实现多种方法的基准代码

更新 2:添加了 Jens Gustedt 方法

基准测试程序:

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

#define N 100000000

int d = 2;

#define POW_2_64 ((double)(1 << 31) * (double)(1 << 31) * 4)

#define calc_b (a + c)
// #define calc_b (a + d)

int main(int argc, char *argv[]) {
    uint64_t a;
    uint64_t c = 0;
    int o = 0;
    int opt;

    if (argc != 2) exit(1);

    opt = atoi(argv[1]);

    switch (opt) {

    case 1: /* faked check, just for timing */
        for (a = 0; a < N; a++) {
            uint64_t b = a + c;
            if (c > a) o++;
            c += b * a;
        }
        break;

    case 2: /* using division */
        for (a = 0; a < N; a++) {
            uint64_t b = a + c;
            if (b && (a > UINT64_MAX / b)) o++;
            c += b * a;
        }
        break;

    case 3: /* using floating point, unreliable */
        for (a = 0; a < N; a++) {
            uint64_t b = a + c;
            if ((double)UINT64_MAX < (double)a * (double)b) o++;
            c += b * a;
        }
        break;

    case 4: /* using floating point and division for difficult cases */
        for (a = 0; a < N; a++) {
            uint64_t b = a + c;
            double m = (double)a * (double)b;
            if ( ((double)(~(uint64_t)(0xffffffff)) < m ) &&
                 ( (POW_2_64 < m) ||
                   ( b &&
                     (a > UINT64_MAX / b) ) ) ) o++;
            c += b * a;
        }
        break;

    case 5: /* Jens Gustedt method */
        for (a = 0; a < N; a++) {
            uint64_t b = a + c;
            uint64_t a1, b1;
            if (a > b) { a1 = a; b1 = b; }
            else       { a1 = b; b1 = a; }
            if (b1 > 0xffffffff) o++;
            else {
                uint64_t a1l = (a1 & 0xffffffff) * b1;
                uint64_t a1h = (a1 >> 32) * b1 + (a1l >> 32);
                if (a1h >> 32) o++;
            }
            c += b1 * a1;
        }
        break;

    default:
        exit(2);
    }
    printf("c: %lu, o: %u\n", c, o);
}

到目前为止,在假设溢出非常不寻常的情况下,使用浮点过滤大多数情况的情况 4 是最快的,至少在我的计算机上它只比不做任何情况的情况慢两倍。

案例 5 比 4 慢 30%,但它始终执行相同的操作,没有任何特殊案例数字需要像 4 那样更慢的处理。

【问题讨论】:

  • 如何使用浮点数,如果结果非常接近 2^64,则进行整数相乘?如果浮点结果高于 9.223370E+18,则精确乘积肯定大于 2^63,如果浮点结果小于 9.223374E+18,则精确乘积肯定小于 3^63。因此,如果浮点结果很接近并且无符号整数乘法的结果大于 1ULL
  • @supercat:这主要是案例 4 所做的。
  • 案例四似乎使用除法来检查结果是否合适。在许多处理器上,无符号整数乘法本身会快得多(整数除法通常是最慢的指令之一)。
  • @supercat:在大多数架构上,浮点寄存器不能准确表示64位整数,所以有一小块区域是a * b &gt;= 2**64但是(double)a * (double)b &lt; 2**64
  • 确实如此,但在该区域内,不换行的unsigned multiply 的结果将大于 UINT64_MAX/2,而进行换行的结果将小于UINT64_MAX/2.

标签: c multiplication integer-overflow


【解决方案1】:

如果您想像 Ambroz 的回答那样避免分裂:

首先你必须看到两个数字中较小的一个,比如a,小于232,否则结果无论如何都会溢出。让b被分解成两个32位字,即b = c 232 + d

那么计算并没有那么困难,我发现:

uint64_t mult_with_overflow_check(uint64_t a, uint64_t b) {
  if (a > b) return mult_with_overflow_check(b, a);
  if (a > UINT32_MAX) overflow();
  uint32_t c = b >> 32;
  uint32_t d = UINT32_MAX & b;
  uint64_t r = a * c;
  uint64_t s = a * d;
  if (r > UINT32_MAX) overflow();
  r <<= 32;
  return addition_with_overflow_check(s, r);
}

所以这是两次乘法,两次移位,一些加法和条件检查。这可能比除法更有效,因为例如两个乘法可以并行流水线化。您必须进行基准测试,看看哪种方法更适合您。

【讨论】:

  • 我认为正确的转变应该是:if (r &gt; UINT32_MAX) overflow(); r &lt;&lt;= 32; 因为之前转变的数字是 c 和 r = a*c !所以我们必须向后移动r。这是错的吗?
  • 确实,if (s &gt; UINT32_MAX) overflow();s &lt;&lt;= 32; 这行不正确,应该使用r 而不是s
【解决方案2】:

它可能无法检测到确切的溢出,但通常您可以在对数尺度上测试乘法结果:

if (log(UINT64_MAX-1) - log(a) - log(b) < 0) overflow_detected(); // subtracting 1 to allow some tolerance when the numbers are converted to double
else prod = a * b;

这取决于您是否真的需要进行精确到 UINT64_MAX 的乘法运算,否则这是检查大数乘法的一种非常便携且方便的方法。

【讨论】:

  • 不需要做慢日志。只要找到a和b的长度(最高1位),如果length(a) + length(b) > 64那么乘法就会溢出
【解决方案3】:
case 6:
    for (a = 0; a < N; a++) {
        uint64_t b = a + c;
        uint64_t a1, b1;
        if (a > b) { a1 = a; b1 = b; }
        else       { a1 = b; b1 = a; }
        uint64_t cc = b1 * a1;
        c += cc;
        if (b1 > 0xffffffff) o++;
        else {
            uint64_t a1l = (a1 & 0xffffffff) + (a1 >> 32);
            a1l = (a1 + (a1 >> 32)) & 0xffffffff;
            uint64_t ab1l = a1l * b1;
            ab1l = (ab1l & 0xffffffff) + (ab1l >> 32);
            ab1l += (ab1l >> 32);
            uint64_t ccl = (cc & 0xffffffff) + (cc >> 32);
            ccl += (ccl >> 32);
            uint32_t ab32 = ab1l; if (ab32 == 0xffffffff) ab32 = 0;
            uint32_t cc32 = ccl; if (cc32 == 0xffffffff) cc32 = 0;
            if (ab32 != cc32) o++;
        }
    }
    break;

此方法将正常乘法的结果(可能溢出)与不会溢出的乘法结果进行比较。所有计算都是模数 (2^32 - 1)。

它比 Jens Gustedt 的方法更复杂并且(很可能)不快。

经过一些小的修改后,它可以乘以 96 位精度(但没有溢出控制)。更有趣的是,这种方法的思想可以用来检查一系列算术运算(乘法、加法、减法)的溢出。

回答了一些问题

首先,关于"your code is not portable"。是的,代码不可移植,因为它使用原始问题中要求的uint64_t。严格来说,您无法使用(u)int64_t 获得任何可移植的答案,因为标准并不要求它。

关于"once some overflow happens, you can not assume the result value to be anything"。标准说未签名的迭代器不能溢出。第 6.2.5 章,第 9 项:

涉及无符号操作数的计算永远不会溢出, 因为不能用生成的无符号整数类型表示的结果是 以比最大值大一的数字为模减少,可以是 由结果类型表示。

所以无符号 64 位乘法以 2^64 为模执行,不会溢出。

现在关于"logic behind"。 “哈希函数”在这里不是正确的词。我只使用以(2^32 - 1) 为模的计算。乘法的结果可以表示为n*2^64 + m,其中m是可见结果,n表示我们溢出了多少。由于2^64 = 1 (mod 2^32 - 1),我们可以计算[true value] - [visible value] = (n*2^64 + m) - m = n*2^64 = n (mod 2^32 - 1)。如果n 的计算值不为零,则存在溢出。如果为零,则没有溢出。只有在n &gt;= 2^32 - 1 之后才有可能发生任何冲突。这永远不会发生,因为我们检查了被乘数之一是否小于2^32

【讨论】:

  • 你能解释一下背后的逻辑吗?我看到你有一个哈希函数,你正在以两种方式计算应该产生相同的结果,除非我们想要捕获的溢出发生。但是,哈希上是否存在一些冲突,可能会让一些溢出未被检测到?除此之外,C 标准规定一旦发生溢出,您不能假设结果值是任何值。具体来说,gcc 进行了假设溢出不会发生的优化。所以你的代码是不可移植的。
  • @salva 我同意,有理由将此代码视为不可移植的。标准对整数溢出不是很清楚(据我了解)。我们只有二进制计算机,它们以通常的方式执行整数乘法,截断结果。所以这个可移植性问题主要是理论上的。在答案中查看更多解释。
  • 这个问题不是理论上的,是很现实的!至少 gcc 假设在优化代码时不会发生溢出。例如,它可以消除代码为if ((uint32_t)12 + some_uint &lt; 10) { do_something(); }
  • 有些误解是因为“溢出”这个词。由于我的算法仅使用无符号值(以及到目前为止的所有其他算法),因此这里没有真正的溢出。有一个截断。并且标准不会将无符号截断视为溢出。看我的报价。这里也没有未定义的行为。正确的编译器不应错误地解释此代码(使用常量和运行时值)。而且代码是可移植的(实际上几乎是可移植的,因为不能保证 uint64_t)。
【解决方案4】:

其实乘法也可以用同样的原理:

uint64_t a;
uint64_t b;
...
if (b != 0 && a > UINT64_MAX / b) { // if you multiply by b, you get: a * b > UINT64_MAX
    < error >
}
uint64_t c = a * b;

对于有符号整数也可以进行类似操作,您可能需要为每个符号组合设置一个大小写。

【讨论】:

  • 除法是一个很慢的操作。我在我的电脑上运行了一些基准测试,它的速度慢了 10 倍以上。
【解决方案5】:

有一些(希望是)有用答案的相关问题:Best way to detect integer overflow in C/C++。此外,它不只涵盖uint64_t ;)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2020-06-12
    • 1970-01-01
    • 2015-03-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-05-21
    • 1970-01-01
    相关资源
    最近更新 更多