Подтвердить что ты не робот

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

Предположим, что у нас есть 2 константы A и B и переменная i, все 64-битные целые числа. И мы хотим вычислить простую общую арифметическую операцию, такую ​​как:

i * A / B    (1)

Чтобы упростить задачу, допустим, что переменная i всегда находится в диапазоне [INT64_MIN*B/A, INT64_MAX*B/A], так что конечный результат арифметической операции (1) не переполняется (т.е. соответствует в диапазоне [INT64_MIN, INT64_MAX]).

Кроме того, i считается более вероятным в дружественном диапазоне Range1 = [INT64_MIN/A, INT64_MAX/A] (то есть: близком к 0), однако i может быть (менее вероятно) снаружи этот диапазон. В первом случае тривиальное целочисленное вычисление i * A не будет переполняться (поэтому мы назвали диапазон дружественным); и в последнем случае тривиальное целочисленное вычисление i * A будет переполняться, что приведет к ошибочному результату при вычислении (1).

Каким будет "самый безопасный" и "самый эффективный" способ вычисления операции (1) (где "безопаснее" означает: сохранение точности или, по крайней мере, достойной точности и где "наиболее эффективным" означает: наименьшее среднее время вычислений), если i более вероятно в диапазоне дружественных Range1.

В настоящее время решение, реализованное в настоящее время в коде, является следующим:

(int64_t)((double)A / B * i)

какое решение является достаточно безопасным (без переполнения), хотя и неточным (прецизионные потери из-за двойного значения 53-разрядного ограничения) и довольно быстро, потому что двойное деление (double)A / B предварительно вычисляется во время компиляции, позволяя вычислять только двойное умножение во время выполнения.

4b9b3361

Ответ 1

Чтобы дать количественный ответ на этот вопрос, я сделал контрольный образец различных решений в рамках предложенных здесь в этом сообщении (благодаря комментариям и ответам).

Контрольный показатель измеряет время вычисления различных реализаций, когда i находится внутри дружественного диапазона Range1= [INT64_MIN/A, INT64_MAX/A], а когда i находится за пределами дружественного диапазона (еще в безопасном диапазоне Range2= [INT64_MIN*B/A, INT64_MAX*B/A]).

Каждая реализация выполняет "безопасное" (т.е. без какого-либо переполнения) вычисления операции: i * A / B (за исключение 1-го осуществления, учитывая, как эталонное время вычислений). Однако некоторые реализации могут возвращать нечастый неточный результат вычисления (какое поведение уведомлено).

Некоторые предлагаемые решения не были протестированы или не перечислены ниже; это: решение с использованием __int128 (неподдерживается компилятором ms vc), но вместо этого используется int128_t; решения с использованием расширенных 80 бит long double (неподдерживаемый компилятором ms vc); решение с использованием InfInt (работает и проверено, хотя слишком медленно, чтобы стать достойным конкурентом).

Измерения времени указаны в ps/op (пикосекунды за операцию). Базовая платформа - это Intel Q6600 @3GHz под Windows 7 x64, исполняемый файл скомпилирован с MS vc14, x64/Release target. Переменные, константы и функции, указанные в дальнейшем, определяются как:

int64_t       i;
const int64_t A     = 1234567891;
const int64_t B     = 4321987;
inline bool   in_safe_range(int64_t i) { return (INT64_MIN/A <= i) && (i <= INT64_MAX/A); }
  • (i * A / B) [ссылка]
    i в Range1: 1469 ps/op, i вне Range1: нерелевантный (переполнение)
  • ((int64_t)((double)i * A / B))
    i в Range1: 10613 ps/op, i вне Range1: 10606 ps/op
    Примечание: нечастый неточный результат (максимальная ошибка = 1 бит) во всем диапазоне Range2
  • ((int64_t)((double)A / B * i))
    i в Range1: 1073 ps/op, i вне Range1: 1071 ps/op
    Примечание: нечастый неточный результат (максимальная ошибка = 1 бит) во всем диапазоне Range2
    Примечание: компилятор, вероятно, предварительно вычислил (double)A / B, что привело к наблюдаемому повышению производительности по сравнению с предыдущим решением.
  • (!in_safe_range(i) ? (int64_t)((double)A / B * i) : (i * A / B))
    i в Range1: 2009 ps/op, i вне Range1: 1606 ps/op
    Примечание: редкий неточный результат (максимальная ошибка = 1 бит) за пределами диапазона1
  • ((int64_t)((int128_t)i * A / B)) [boost int128_t]
    i в Range1: 89924 ps/op, i вне Range1: 89289 ps/op
    Примечание: форматирование int128_t сильно сказывается на платформе сканера (не знаю, почему)
  • ((i / B) * A + ((i % B) * A) / B)
    i в Range1: 5876 ps/op, i вне Range1: 5879 ps/op
  • (!in_safe_range(i) ? ((i / B) * A + ((i % B) * A) / B) : (i * A / B))
    i в Range1: 1999 ps/op, i вне Range1: 6135 ps/op

Заключение
a) Если небольшие ошибки вычислений приемлемы во всем диапазоне Range2, то решение (3) является самым быстрым, даже быстрее, чем вычисление прямого целого, указанное в качестве ссылки. < ш > b) Если ошибки вычисления неприемлемы в дружественном диапазоне Range1, но приемлемо вне этого диапазона, то решение (4) является самым быстрым. c) Если ошибки вычисления неприемлемы во всем диапазоне Range2, то решение (7) выполняет также решение (4) в дружественном диапазоне Range1 и остается прилично быстрой вне этого диапазона.

Ответ 2

Если вы не можете получить более высокие оценки на диапазонах, то лучше всего следовать советам iammilind, чтобы использовать __int128.

Причина в том, что в противном случае вам нужно было бы реализовать полную логику слова для умножения двойного слова и двойного слова путем разделения слов. Руководства для процессоров Intel и AMD содержат полезную информацию и готовый код, но он довольно активно участвует, а использование C/С++ вместо ассемблера делает вещи вдвойне сложными.

Все хорошие компиляторы раскрывают полезные примитивы как внутренние. Список Microsoft, похоже, не содержит примитив, подобный muldiv, но __mul128 intrinsic дает две половинки 128-битного продукта в виде двух 64-битных целых чисел. Исходя из этого, вы можете выполнить длинное деление двух цифр на одну цифру, где одна цифра будет 64-битным целым числом (обычно называемым "конечность", потому что больше, чем цифра, но все еще только часть целого). Все еще довольно активно, но намного лучше, чем использование чистого C/С++. Однако переносимость - это не лучше, чем использование __int128 напрямую. По крайней мере, так, что разработчики компилятора уже сделали для вас всю тяжелую работу.

Если ваш домен приложения может дать вам полезные оценки, например, (u % d) * v не будет переполняться, вы можете использовать идентификатор

(u * v) / d = (u / d) * v + ((u % d) * v) / d

где / означает целочисленное деление, если u неотрицательно и d положительно (в противном случае вы можете столкнуться с свободой, допустимой для семантики оператора %).

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

Кроме того, с неподписанными типами вы можете отказаться от таких правил, как s = a (+) b (где (+) возможно переполненное беззнаковое дополнение), у вас будет либо s == a + b, либо s < a && s < b, что позволяет обнаруживать переполнение после факт с дешевыми операциями.

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

Ответ 3

Я думаю, вы можете обнаружить переполнение до того, как это произойдет. В вашем случае i * A / B вас беспокоит только часть i * A, потому что разделение не может переполняться.

Вы можете обнаружить переполнение, выполнив проверку bool overflow = i > INT64_MAX / A. Вам нужно будет изменить это в зависимости от знака операндов и результата.

Ответ 4

Некоторые реализации позволяют __int128_t. Проверьте, позволяет ли ваша реализация, чтобы вы могли использовать его в качестве заполнителя вместо double. См. Ниже сообщение:
Почему нет int128_t?


Если вас не очень беспокоит "быстрая", то для хорошей переносимости я бы предложил использовать только заголовки только библиотеки С++ "InfInt" .

Довольно просто использовать библиотеку. Просто создайте экземпляр класса InfInt и начните его использовать:

InfInt myint1 = "15432154865413186646848435184100510168404641560358"; 
InfInt myint2 = 156341300544608LL;

myint1 *= --myint2 - 3;
std::cout << myint1 << std::endl;

Ответ 5

Не уверен в значениях границ, будет (i / B) * A + (i % B) * A / B help?