首先,整数乘法确实非常便宜。只要每个循环迭代有多个工作周期和一个备用执行槽,它就应该通过在大多数非小型处理器上重新排序而完全隐藏。
如果您确实有一个整数乘法速度非常慢的处理器,那么一个真正聪明的编译器可能会将您的循环转换为:
for (uint64_t i = 0, j = 0; j < cut; j += 2*i+1, i++)
将乘法替换为lea
或一个班次和两个加法。
撇开这些注释不谈,让我们看看你所说的问题。不,你不能只使用i < sqrt(n)
。反例:n = 0x20000000000000
。假设遵守 IEEE-754,您将拥有cut = 0x5a82799
, and cut*cut
is 0x1ffffff8eff971
.
然而,基本的浮点误差分析表明计算中的误差sqrt(n)
(转换为整数之前)以 ULP 的 3/4 为界。所以您可以安全地使用:
uint32_t cut = sqrt(n) + 1;
并且您最多将执行一次额外的循环迭代,这可能是可以接受的。如果您想完全精确,请改用:
uint32_t cut = sqrt(n);
cut += (uint64_t)cut*cut < n;
Edit: z boson澄清说,就他的目的而言,这仅在以下情况下才重要:n
是一个精确的平方(否则,得到的值为cut
“太小了”是可以接受的)。在这种情况下,不需要进行调整,可以安全地使用:
uint32_t cut = sqrt(n);
为什么这是真的?实际上,这很简单。转换n
to double
引入扰动:
double_n = n*(1 + e)
这满足|e| < 2^-53
。该值的数学平方根可以展开如下:
square_root(double_n) = square_root(n)*square_root(1+e)
现在,自从n
假设是最多 64 位的完美平方,square_root(n)
是一个最多 32 位的精确整数,并且是我们希望计算的数学精确值。来分析square_root(1+e)
术语,使用泰勒级数1
:
square_root(1+e) = 1 + e/2 + O(e^2)
= 1 + d with |d| <~ 2^-54
因此,数学上的精确值square_root(double_n)
与[1]所需的精确答案的距离不到 ULP 的一半,并且必须舍入到该值。
[1] 我在这里滥用相对误差估计,其中 ULP 的相对大小实际上在二进制文件中有所不同 - 我试图在不陷入太多困境的情况下提供一点证明的味道往下看细节。这一切都可以做得非常严格,只是对于 Stack Overflow 来说有点啰嗦。