计算可能溢出的整数运算的最安全,最有效的方法

时间:2016-04-24 17:39:08

标签: c++ c

假设我们有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,只计算了一个双乘法在运行时。

5 个答案:

答案 0 :(得分:6)

如果您无法在所涉及的范围内获得更好的界限,那么您最好关注iammilind's advice以使用__int128

原因在于,否则你必须实现双字乘法和双字逐字的完整逻辑。英特尔和AMD处理器手册包含有用的信息和现成的代码,但它非常复杂,使用C / C ++而不是汇编程序会使事情变得更加复杂。

所有优秀的编译器都将有用的原语作为内在函数。 Microsoft's list似乎不包含类似muldiv的原语,但__mul128内在函数将128位乘积的两半作为两个64位整数。基于此,您可以执行两位数的长除以一位数,其中一个“数字”将是64位整数(通常称为“肢体”,因为大于数字但仍然只是整数的一部分)。仍然相当复杂,但比使用纯C / C ++要好很多。但是,可移植性并不比直接使用__int128好。至少那种方式,编译器实现者已经为你完成了所有艰苦的工作。

如果您的应用程序域可以为您提供有用的界限,那么(u % d) * v不会溢出,那么您可以使用身份

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

其中/表示整数除法,只要u为非负且d为正(否则您可能会与运算符%的语义允许的余地相冲突)。

在任何情况下,您可能必须分离出操作数的符号并使用无符号运算,以便找到可以利用的更有用的机制 - 或者避免编译器的破坏,例如您提到的饱和乘法。有符号整数运算的溢出会调用未定义的行为,编译器可以随心所欲地执行任何操作。相比之下,无符号类型的溢出是明确定义的。

此外,对于未签名的类型,您可以使用s = a (+) b(其中(+)可能会溢出无符号添加)的规则回退,您将拥有s == a + b或{{1} },它可以让您通过廉价操作检测溢出。

然而,你不太可能在这条道路上走得更远,因为所需的努力很快接近 - 甚至超过 - 我实施前面提到的双肢操作的努力。只有对应用程序域进行全面分析才能提供规划/部署此类快捷方式所需的信息。在一般情况下,你给出的界限几乎没有运气。

答案 1 :(得分:3)

为了提供问题的量化答案,我在本文中提出了不同解决方案的基准,作为本文提出的解决方案的一部分(感谢评论和答案)。

基准测量不同实现的计算时间,i友好范围 Range1 = [INT64_MIN/A, INT64_MAX/A]内,以及{{1}时}}在友好范围之外(但在安全范围 Range2 = i)。

每个实施都执行一个" safe" (即没有溢出)计算操作:[INT64_MIN*B/A, INT64_MAX*B/A](第一种实现除外,作为参考计算时间给出)。但是,某些实现可能会返回不常见的不准确计算结果(通知该行为)。

提出的一些解决方案尚未经过测试或未在下文列出;这些是:使用i * A / B的解决方案(ms vc编译器不支持),但已使用boost __int128;使用扩展80位int128_t的解决方案(ms vc编译器不支持);使用long double的解决方案(工作和测试虽然太慢而不能成为一个像样的竞争对手)。

时间测量以ps / op指定(每次操作的皮秒)。 Benchmark平台是Windows 7 x64下的Intel Q6600 @ 3GHz,可执行文件使用MS vc14,x64 / Release target编译。以下引用的变量,常量和函数定义为:

InfInt
  1. 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 Range1 1469 ps / op i Range1 无关紧要(溢出)
  2. (i * A / B)
    Range1 10613 ps / op i Range1 10606 ps / op
    注意:整个范围内不常见的不准确结果(最大错误= 1位) Range2
  3. ((int64_t)((double)i * A / B))
    Range1 1073 ps / op i Range1 1071 ps / op
    注意:整个范围 Range2 中不常见的不准确结果(最大错误= 1位) 注意:编译器可能预先计算((int64_t)((double)A / B * i)),导致观察到的性能提升与之前的解决方案相比。
  4. (double)A / B
    Range1 2009 ps / op i Range1 1606 ps / op
    注意: Range1
  5. 之外的罕见不准确结果(最大错误= 1位)
  6. (!in_safe_range(i) ? (int64_t)((double)A / B * i) : (i * A / B)) [提升((int64_t)((int128_t)i * A / B))]
    Range1 89924 ps / op i Range1 89289 ps / op
    注意:提升int128_t在替补平台上表现非常糟糕(不知道为什么)
  7. int128_t
    Range1 5876 ps / op i Range1 5879 ps / op
  8. ((i / B) * A + ((i % B) * A) / B)
    Range1 1999 ps / op i Range1 6135 ps / op
  9.   

    <强>结论
      a)如果在整个范围 Range2 中可以接受轻微的计算误差,则解(3)是最快的,甚至比作为参考给出的直接整数计算更快。登记/>   b)如果计算错误在友好范围 Range1 中是不可接受的,但在此范围之外仍可接受,则解决方案(4)是最快的。
      c)如果计算错误在整个范围 Range2 中是不可接受的,则解决方案(7)以及友好中的解决方案(4)执行范围 Range1 ,并且在此范围之外保持适当的快速。

答案 2 :(得分:1)

I think you can detect the overflow before it happens. In your case of i * A / B, you are only worried about the i * A part because the division cannot overflow.

You can detect the overflow by performing test of bool overflow = i > INT64_MAX / A. You will have to do modify this depending on the sign of operands and result.

答案 3 :(得分:1)

Some implementations permit __int128_t. Check if your implementation allows it, so that you can you may use it as placeholder instead of double. Refer below post:
Why isn't there int128_t?


If not very concerned about "fast"-ness, then for good portability I would suggest to use header only C++ library "InfInt".

It is pretty straight forward to use the library. Just create an instance of InfInt class and start using it:

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

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

答案 4 :(得分:0)

不确定值限制,(i / B) * A + (i % B) * A / B会有帮助吗?