假设我们有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
,只计算了一个双乘法在运行时。
答案 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
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)
((int64_t)((double)i * A / B))
((int64_t)((double)A / B * i))
,导致观察到的性能提升与之前的解决方案相比。(double)A / B
(!in_safe_range(i) ? (int64_t)((double)A / B * i) : (i * A / B))
[提升((int64_t)((int128_t)i * A / B))
] int128_t
在替补平台上表现非常糟糕(不知道为什么)int128_t
((i / B) * A + ((i % B) * A) / B)
<强>结论强>
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
会有帮助吗?