例如,由于浮点数的精度,下面的代码将给出不希望的结果。
double a = 1 / 3.0;
int b = a * 3; // b will be 0 here
我想知道如果我使用数学函数,类似的问题是否会出现。例如
int a = sqrt(4); // Do I have guarantee that I will always get 2 here?
int b = log2(8); // Do I have guarantee that I will always get 3 here?
如果没有,如何解决这个问题?
编辑:
事实上,我在为一个算法任务编程时遇到了这个问题。我想买
最大的整数,它是2的幂,并且小于或等于整数N
所以圆函数不能解决我的问题。我知道我可以通过循环来解决这个问题,但它似乎不是很优雅。
我想知道
int a = pow(2, static_cast<int>(log2(N)));
总是能给出正确的结果。例如,如果N=8,那么log2(N)是否可能给我2.9999999999999的结果,最终结果变成4而不是8?
操作数不准确与结果不准确
我想知道如果我使用数学函数,类似的问题是否会出现。
实际上,对于基本操作(包括*
),不存在可能阻止log2(8)
为3的问题。但它是为log2
函数而存在的。
你混淆了两个不同的问题:
double a = 1 / 3.0;
int b = a * 3; // b will be 0 here
在上面的例子中,a
并不完全是1/3,因此a*3
可能不产生1.0
。该产品可能碰巧达到1.0
,但事实并非如此。然而,如果a
在某种程度上正好是1/3,那么a
乘以3的乘积将恰好是1.0
,因为这就是IEEE 754浮点的工作方式:基本运算的结果是与相同操作数上的相同运算的数学结果最接近的可表示值。当确切的结果可以用浮点数表示时,那么这个表示就是你得到的。
sqrt和log2的准确性
sqrt
是"基本运算"的一部分,因此在IEEE 754系统中,sqrt(4)
总是被保证为2.0
,无一例外。
log2
不是基本操作的一部分。IEEE 754标准不能保证实现该函数的结果最接近数学结果。它可以是更远的另一个可表示的数字。因此,如果没有更多关于log2
函数的假设,就不可能知道log2(8.0)
是什么
然而,对于诸如log2
之类的基本函数,大多数具有合理质量的实现保证了实现的结果在数学结果的1 ULP之内。当数学结果不可表示时,这意味着上面的可表示值或下面的值(但不一定是两者中最接近的值)。当数学结果是可精确表示的(例如3.0
)时,则该表示仍然是唯一保证返回的表示。
因此,关于log2(8)
,答案是"如果你有一个合理质量的log2
实现,你可以预期结果是3.0`"
不幸的是,并不是每个基本功能的每个实现都是高质量的实现。请参阅这篇博客文章,这是由于pow
的一个广泛使用的实现在计算pow(10.0, 2.0)
时不准确超过1个ULP,从而返回99.0
而不是100.0
。
四舍五入到最接近的整数
接下来,在每种情况下,通过隐式转换将浮点值分配给int
。这种转换在C++标准中被定义为截断浮点值(即向零取整)。如果您希望浮点计算的结果是一个整数,您可以在分配之前将浮点值四舍五入到最接近的整数。这将有助于在所有错误未累积到大于1/2的值的情况下获得所需答案:
int b = std::nearbyint(log2(8.0));
最后,用一个简单的答案来回答这个问题——标题:是的,当使用浮点函数来产生积分最终结果时,你应该担心准确性。这些功能甚至没有基本操作所附带的保证。
不幸的是,C++中从浮点数字到整数的默认转换非常疯狂,因为它通过删除小数部分来实现。
这很糟糕,有两个原因:
-
一个真正接近正整数的浮点数,但低于它将被转换为前一个整数(例如3-1×10-10=2.9999999999将被转换成2)
-
一个真正接近负整数的浮点数,但在它之上将被转换为下一个整数(例如-3+1×10-10=-2.9999999999将转换为-2)
(1)和(2)的组合也意味着使用int(x + 0.5)
将不合理,因为它会将负数四舍五入。
有一个合理的round
函数,但不幸的是,它返回了另一个浮点数,因此需要编写int(round(x))
。
使用C99或C++11时,可以使用lround(x)
。
请注意,在浮点中唯一可以正确表示的数字是商,其中分母是2的整数幂。
例如,1/65536 = 0.0000152587890625
可以被正确地表示,但即使只是0.1
也不可能被正确地表达,因此涉及该量的任何计算都将被近似。
当然,当使用0.1近似可以抵消偶尔留下的正确结果时,但当使用IEEE754双精度浮点数进行计算时,即使只是将0.1的十倍相加也不会得到1.0的结果。
更糟糕的是,编译器被允许对中间结果使用更高的精度。这意味着,如果编译器决定使用更高的精度并在末尾四舍五入到最接近的双精度,则在转换为整数时,加10乘以0.1可能会返回1。
这是"更糟糕的",因为尽管精度更高,但结果取决于编译器和编译器选项,这使得对计算的推理更加困难,并使确切的结果在不同系统之间不可移植(即使它们使用相同的精度和格式)。
大多数编译器都有特殊的选项来避免这个特定的问题。