Есть ли какой-либо эффективный и переносимый способ проверить, когда операции умножения с операндами int64_t или uint64_t переполняются в C?
Например, для добавления uint64_t я могу:
if (UINT64_MAX - a < b) overflow_detected();
else sum = a + b;
Но я не могу найти подобное простое выражение для умножения.
Все, что мне приходит в голову, - это разбивать операнды на высокие и низкие части uint32_t и выполнять умножение этих частей при проверке переполнения, что-то действительно уродливое и, вероятно, тоже неэффективное.
Обновление 1: добавлен тестовый код, реализующий несколько подходов.
Обновление 2: добавлен метод Йенса Густедта
программа бенчмаркинга:
#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 на 30% медленнее, чем 4, но он всегда работает одинаково, нет каких-либо специальных номеров случаев, которые требуют более медленной обработки, как это происходит с 4.
a * b >= 2**64
, но(double)a * (double)b < 2**64
- person salva   schedule 26.02.2014unsigned multiply
, который не переносится, будет больше, чем UINT64_MAX / 2, а результат того, который выполняет перенос, будет меньше, чем UINT64_MAX / 2. - person supercat   schedule 26.02.2014