обнаружение умножения переполнения целых чисел uint64 t на C

есть ли эффективный и портативный способ проверить, когда операции умножения с переполнением операндов int64_t или uint64_t в C?

например, для добавления uint64_t я могу сделать:

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

но я не могу добраться до аналогичного простого выражения для умножения.

все, что мне приходит в голову, - это разбить операнды на высокие и низкие части uint32_t и выполнить умножение этих частей при проверке переполнения, что-то действительно уродливый и, вероятно, неэффективный.

обновление 1: добавлен некоторый эталонный код, реализующий несколько подходов

обновление 2: способ Йенс 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: %un", c, o);
}

до сих пор случай 4, который использует плавающую точку для фильтрации большинства случаев, является самым быстрым, когда предполагается, что переполнения очень необычны, по крайней мере, на моем компьютере, где он только в два раза медленнее, чем ничего не делать случай.

случай 5, на 30% медленнее, чем 4, но он всегда выполняет то же самое, нет никаких номеров особых случаев, которые требуют более медленной обработки, как это происходит с 4.

5 ответов


если вы хотите избежать разделения, как в ответе Амброза:

сначала вы должны увидеть, что меньшее из двух чисел, скажем 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);
}

так это два умножения, две смены, некоторые дополнения и проверка состояния. Это может быть более эффективным, чем разделение, потому что e.g два умножения могут быть конвейеризованы в paralle. Вам нужно будет проверить, что работает лучше для вас.


фактически, тот же принцип можно использовать для умножения:

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;

для целых чисел со знаком аналогично можно сделать, вам, вероятно, понадобится случай для каждой комбинации знаков.


вопрос с некоторыми (надеюсь) полезные ответы: лучший способ обнаружить переполнение целого числа в C / C++. Плюс он не покрывает uint64_t только ;)


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

это сложнее и (скорее всего) не быстрее, чем метод Йенс 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". Стандарт говорит, что неподписанные itegers не могут переполняться. Глава 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.


он может не обнаруживать точные переполнения, но в целом вы можете проверить результат вашего умножения на логарифмической шкале:

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, иначе это очень портативный и удобный способ проверить умножение больших чисел.