在使用int64_t或uint64_t操作数的乘法运算在C中溢出时,是否有任何有效且可移植的方法来检查?
例如,为了添加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所示。
答案 0 :(得分:9)
实际上,相同的原理可以用于乘法:
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;
对于签名整数,可以做类似的事情,你可能需要为每个符号组合提供一个案例。
答案 1 :(得分:5)
如果你想在Ambroz的答案中避免分裂:
首先你必须看到两个数字中较小的一个,比如a
,小于2 32 ,否则结果无论如何都会溢出。将b
分解为两个32位字b
= c
2 32 + 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);
}
所以这是两次乘法,两次移位,一些加法和条件检查。这可能比分割更有效,因为例如两个乘法可以并行流水线化。你必须进行基准测试才能看出哪种方法更适合你。
答案 2 :(得分:1)
与一些(希望)有用的答案相关的问题:Best way to detect integer overflow in C/C++。此外,它不仅涵盖uint64_t
;)
答案 3 :(得分:1)
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"
。 Standard表示无符号迭代不能溢出。第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 >= 2^32 - 1
之后才能进行任何碰撞。这将永远不会发生,因为我们检查其中一个被乘数小于2^32
。
答案 4 :(得分:1)
它可能无法检测到确切的溢出,但通常您可以在对数刻度上测试乘法的结果:
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,否则这是检查大数乘法的一种非常便携和方便的方法。