обнаружение умножения переполнения целых чисел 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: добавлен метод Йенса Густедта

программа бенчмаркинга:

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


person salva    schedule 16.12.2011    source источник
comment
Как насчет использования чисел с плавающей запятой и, если результат очень близок к 2 ^ 64, умножения целых чисел? Если результат с плавающей запятой выше 9,223370E + 18, точное произведение обязательно будет больше 2 ^ 63, а если результат с плавающей запятой меньше 9,223374E + 18, точное произведение наверняка будет меньше 3 ^ 63. Таким образом, если результат с плавающей запятой близок, а целочисленное умножение без знака дает больше 1ULL ‹---------------- 63, целочисленный результат не будет представлять собой переполнение.   -  person supercat    schedule 26.02.2014
comment
@supercat: это в основном то, что делает случай 4.   -  person salva    schedule 26.02.2014
comment
В четвертом случае, похоже, используется деление, чтобы проверить, подходит ли результат. На многих процессорах выполнение целочисленного умножения без знака будет намного быстрее (целочисленное деление часто является одной из самых медленных инструкций).   -  person supercat    schedule 26.02.2014
comment
@supercat: на большинстве архитектур регистры с плавающей запятой не могут точно представлять 64-битные целые числа, поэтому есть небольшая область, где a * b >= 2**64, но (double)a * (double)b < 2**64   -  person salva    schedule 26.02.2014
comment
Это действительно так, но в этой области результат unsigned multiply, который не переносится, будет больше, чем UINT64_MAX / 2, а результат того, который выполняет перенос, будет меньше, чем UINT64_MAX / 2.   -  person supercat    schedule 26.02.2014


Ответы (5)


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

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

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

person Jens Gustedt    schedule 16.12.2011
comment
Я думаю, что правильный сдвиг должен быть: if (r > UINT32_MAX) overflow(); r <<= 32;, потому что число, сдвинутое до этого, - c, а r = a * c! поэтому мы должны сдвинуть назад r. это неправильно? - person Hicham; 20.12.2011
comment
Действительно, строки if (s > UINT32_MAX) overflow(); и s <<= 32; неверны, они должны использовать r вместо s. - person Adam Bowen; 16.05.2013

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

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;

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

person Ambroz Bizjak    schedule 16.12.2011
comment
Деление - очень медленная операция. Я провел несколько тестов на своем компьютере, и он работает более чем в 10 раз медленнее. - person salva; 16.12.2011

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

person Sebastian Dressler    schedule 16.12.2011

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

Он сложнее и (скорее всего) не быстрее, чем метод Йенса Густедта.

После небольших изменений он может умножаться с точностью до 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 >= 2^32 - 1. Этого никогда не произойдет, поскольку мы проверяем, что одно из множителей меньше 2^32.

person Evgeny Kluev    schedule 20.12.2011
comment
Можете ли вы объяснить логику? Я вижу, что у вас есть хэш-функция, которую вы вычисляете двумя способами, которая должна давать тот же результат, если только не произойдет переполнение, которое мы хотим поймать. Но может ли быть какая-то коллизия в хеш-коде, из-за которой переполнение может пройти незамеченным? Кроме того, стандарт C говорит, что если произойдет какое-то переполнение, вы не можете предполагать, что значение результата будет чем-либо. В частности, gcc выполняет оптимизацию, предполагающую, что переполнения не происходит. Итак, ваш код не переносится. - person salva; 20.12.2011
comment
@salva Согласен, есть причины считать этот код непереносимым. Стандарт не очень ясно насчет целочисленного переполнения (насколько я понимаю). У нас есть только бинарные компьютеры, и они производят целочисленное умножение обычным способом, усекая результат. Так что эта проблема переносимости в основном теоретическая. См. Дополнительные объяснения в ответе. - person Evgeny Kluev; 20.12.2011
comment
Это проблема не теоретическая, а вполне реальная! По крайней мере, gcc предполагает, что переполнения никогда не происходит, когда он оптимизирует ваш код. Например, он может исключить код как if ((uint32_t)12 + some_uint < 10) { do_something(); } - person salva; 20.12.2011
comment
Некоторое недоразумение связано с переполнением слова. Поскольку мой алгоритм использует только беззнаковые значения (как и все остальные алгоритмы), здесь нет реального переполнения. Есть усечение. И стандарт не рассматривает беззнаковое усечение как переполнение. Смотрите мою цитату. Также здесь нет неопределенного поведения. Правильный компилятор не должен интерпретировать этот код неправильно (как с константами, так и со значениями времени выполнения). И код переносимый (на самом деле почти переносимый, потому что uint64_t не гарантируется). - person Evgeny Kluev; 21.12.2011

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

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

person Bort    schedule 22.12.2011
comment
нет необходимости делать медленные журналы. Просто найдите длину (верхний 1 бит) a и b, если длина (a) + длина (b)> 64, тогда умножение будет переполняться - person phuclv; 04.03.2014