在 C++ 中使用 floor 函数的舍入错误

Posted

技术标签:

【中文标题】在 C++ 中使用 floor 函数的舍入错误【英文标题】:Rounding error using the floor function in C++ 【发布时间】:2016-02-01 14:06:49 【问题描述】:

有人问我以下代码的输出是什么:

floor((0.7+0.6)*10);

返回 12。

我知道浮点表示法不允许以无限精度表示所有数字,我应该预料到会有一些差异。

我的问题是:

    我怎么知道这段代码返回的是 12,而不是 13?为什么 (0.7+0.6)*10 比 13 有点,而不是

    我可以预期地板功能何时无法正常工作以及何时可以确定正常工作?

注意:我不是在问浮动表示的样子或为什么输出不完全是 13。我想知道我应该如何推断 (0.7+0.6)*10 有点 less 大于 13。

【问题讨论】:

请注意,在这种情况下,您可以使用0.7 * 10 + 0.6 * 10 来避免它。一般来说,您可以通过选择一个小因子 epsilon(如 1E-8)并比较 x - epsilonx + epsilon 的下限来检测这一点——如果它们不同,很可能是由于这种影响。 @CompuChip 不,这不能“一般”用单个 epsilon 完成,因为浮点数的精度取决于所涉及数字的大小 @jofel 指出,对于 IEEE754,您可以假设在符合该标准的每台机器上某些计算都是相同的;但是没有捷径可以不做就知道 OP 的操作结果,无论是在纸上,模拟 CPU 对位的操作,还是使用 CPU 本身。 @MarcusMüller 我的建议还是要谨慎一点:如果你在两边都加上 delta,那么你肯定会遇到这种情况。当然,这不排除误报,如果你的计算结果实际上接近一个整数,这将发生。 您可以看到0.6 和0.7 最接近的 IEEE 754 表示,它们的总和不等于 1.3。手动进行转换是一项相当大的工作,虽然可以做到。 【参考方案1】:

我怎么知道这段代码返回的是 12,而不是 13?为什么 (0.7+0.6)*10 比 13 少一点,而不是多一点?

假设您的编译平台严格使用 IEEE 754 标准格式和操作。然后,将所有涉及的常数转换为二进制,保留 53 个有效数字,并应用 IEEE 754 中定义的基本运算,通过计算数学结果并在每一步舍入到 53 个有效二进制数字。任何阶段都不需要计算机参与,但您可以使用 C99 的十六进制浮点格式进行输入和输出,让您的生活更轻松。

我可以预计 floor 函数在什么时候不能正常工作,什么时候可以确定正常工作?

floor() 完全适用于所有正论点。它在您的示例中正常工作。令你惊讶的行为并非源自floor,与floor 无关。令人惊讶的行为始于 6/10 和 7/10 不能完全表示为二进制浮点值这一事实,并且由于这些值具有长扩展,浮点运算 +*可以产生一个稍微四舍五入的结果 wrt 你可以从它们实际应用的参数中期望的数学结果。 floor() 是代码中唯一不涉及近似的地方。

查看发生了什么的示例程序:

#include <stdio.h>
#include <math.h>

int main(void) 
  printf("%a\n%a\n%a\n%a\n%a\n",
         0.7,
         0.6,
         0.7 + 0.6,
         (0.7+0.6)*10,
         floor((0.7+0.6)*10));

结果:

0x1.6666666666666p-1 0x1.3333333333333p-1 0x1.4ccccccccccccp+0 0x1.9ffffffffffffp+3 0x1.8p+3

IEEE 754 双精度实际上是相对于二进制定义的,但为简洁起见,有效数字以十六进制编写。 p 之后的指数代表 2 的幂。例如,最后两个结果的形式都是 *23

0x1.8p+3 是 12。下一个整数 13 是 0x1.ap+3,但计算并未完全达到该值,因此 floor() 的行为是向下舍入到 12。

【讨论】:

【参考方案2】:
    我怎么知道这段代码返回的是 12,而不是 13?

您应该知道它可以并且可能是 12 或 13。您可以通过在给定的 cpu 上进行测试来验证。

通常,您无法知道该值是什么,因为 C++ 标准没有指定浮点数的表示。如果您知道给定架构的格式(例如 IEEE 754),那么您可以手动执行计算,但该结果仅适用于该特定表示。

为什么 (0.7+0.6)*10 比 13 少一点,而不是多一点?

这是一个实现细节,对程序员来说不是有用的知识。您只需要知道它可能是。依赖于其中之一的知识会使您依赖于实现细节。

    我可以预期 floor 功能在什么时候不能正常工作,什么时候可以确定正常工作?

它总是正确地工作,这取决于它被指定的工作方式。

现在,谈到您期望看到的价值。如果您知道您的数字非常接近整数,但可能由于表示错误而略有偏差,您可以在地板前添加0.5

double calculated_integer = (0.7+0.6)*10;
floor(calculated_integer + 0.5);

这样,你总会得到期望值,除非错误超过0.5,这将是一个相当大的错误。

如果您不知道结果应该是整数,那么您只需要接受floorceil 操作会将计算的最大误差增加到1.0 的事实。

【讨论】:

【参考方案3】:

有像IEEE floating point standard 这样的标准,它试图使浮点计算至少有点预测性 通过定义规则,应该如何执行加法和舍入等操作。 要知道结果,您需要计算表达式 根据标准规则。那么你可以确定, 它在每台执行标准的机器上给出相同的结果。

【讨论】:

【参考方案4】:

我怎么知道这段代码返回的是 12,而不是 13?

因为这取决于所涉及的数字,通过尝试

为什么 (0.7+0.6)*10 比 13 少一点,而不是多一点?

嗯,因为这是计算的结果。

我可以预计 floor 函数在什么时候不能正常工作,什么时候可以确定正常工作?

确定正确:仅在 2 的幂的倍数上,如果您的浮点数以二进制表示

真正消除所有的困惑:

不计算就无法知道结果;这取决于所涉及的机器/算法和数量。

【讨论】:

@user2738748:这取决于机器和所涉及的数量。 不尝试就无法知道这一点 你只能通过尝试来证明。你得到 same 结果并不奇怪,但没有规则可以给出结果——例如,(0.3+0.7)*1.3 可能 > 13。没有规则,没有证明除了模拟您的计算机在数字上所做的事情。 @user2738748 如果您知道所有操作数的精确按位实现,您可以为给定的实现证明它。 @jofel:也许我脑子有问题,但是 >=0.1 是从哪里来的? @MarcusMüller 抱歉,应该 >=1(乘以 10 之前的 0.1)。【参考方案5】:

非常简短的回答:你不能。这取决于平台和在该平台上使用的 float iso。

【讨论】:

【参考方案6】:

一般来说,你不能。根本问题是从文本表示到浮点值的转换通常没有尽可能准确地实现。这部分是动力,部分是因为获得最接近文本中表示的值的浮点值可能会很昂贵,在某些情况下需要大整数计算。因此,转换通常与理想值相差几个 ULP(即低端位),而您无法先验预测。因此,该代码将产生什么问题是无法回答的。它应该产生什么的问题可能更容易处理,但这仍然是一种浪费时间的练习。

【讨论】:

以上是关于在 C++ 中使用 floor 函数的舍入错误的主要内容,如果未能解决你的问题,请参考以下文章

C++ 舍入算法中的 0.501,C++ 中的 Excel ROUND

c集成程序中的舍入错误

GCC 不断抱怨 AVX512 函数 _mm512_cvt_roundpd_epi64 的“错误:不正确的舍入操作数”

Powershell:将分数转换为整数 - 令人惊讶的舍入行为

java中的舍入双[重复]

如何处理 vb.net 中双精度数的舍入错误?