【问题标题】:Accuracy of Adding Floats vs. Multiplying Float by Integer浮点数相加与浮点数乘以整数的精度
【发布时间】:2016-06-01 11:43:21
【问题描述】:

在我的计算机科学课程中,我们正在研究浮点数以及它们在内存中的表示方式。我已经了解它们在内存中的表示方式(尾数/有效数、指数及其偏差以及符号位),并且我了解浮点数是如何相互相加和相减的(非规范化和所有有趣的东西)。然而,在查看一些学习问题时,我发现了一些我无法解释的东西。

当一个无法精确表示的浮点数被多次添加到自身时,答案会低于我们在数学上的预期,但是当同一个浮点数乘以一个整数时,答案就会精确到正确的数字。

这是我们学习问题中的一个示例(该示例是用 Java 编写的,为简单起见,我对其进行了编辑):

float max = 10.0f; /* Defined outside the function in the original code */
float min = 1.0f; /* Defined outside the function in the original code */
int count = 10; /* Passed to the function in the original code */
float width = (max - min) / count;
float p = min + (width * count);

在这个例子中,我们被告知结果正好是10.0。但是,如果我们将此问题视为浮点数的总和,我们会得到稍微不同的结果:

float max = 10.0f; /* Defined outside the function in the original code */
float min = 1.0f; /* Defined outside the function in the original code */
int count = 10; /* Passed to the function in the original code */
float width = (max - min) / count;

for(float p=min; p <= max; p += width){
    System.out.printf("%f%n", p);
}

我们被告知,在这个测试中p 的最终值为~9.999999p 的最后一个值与max 的值之间的差值为-9.536743E-7。从逻辑的角度来看(知道浮点数是如何工作的),这个值是有意义的。

不过,我不明白的是,为什么我们在第一个示例中正好得到 10.0。从数学上讲,我们得到 10.0 是有道理的,但是知道浮点数是如何存储在内存中的,这对我来说没有意义。谁能解释为什么我们通过将不精确的浮点数与 int 相乘来得到精确的值?

编辑为了澄清,在最初的研究问题中,一些值被传递给函数,而其他值被声明在函数之外。我的示例代码是研究问题示例的缩短和简化版本。因为有些值是传入函数而不是显式定义为常量,所以我相信可以排除编译时的简化/优化。

【问题讨论】:

  • 因为编译器将所有这些都减少为一个常量值。尝试将每个语句都设为函数,然后一个接一个地调用。
  • @Amit,我很抱歉,我应该在我的问题中说清楚。示例中定义的一些值作为变量传递给计算最终结果的函数,因此它似乎不太可能是编译器优化。我试图简化这篇文章的代码,所以我在示例中定义了值。我会尽快进行编辑以澄清这一点。
  • 除非您的编辑让我感到惊讶,否则我的评论(如果您愿意,我会将其作为答案发布)仍然有效。编译器会将所有语句优化为max 值,因为所有语句都会进行来回计算。
  • 他可以通过在命令行或文件中输入数字来排除这种情况,因此它们是变量而不是编译时间常数。
  • 我确定他们想教您的是浮点数已损坏且需要小心,因为您无法以 2 为基数的浮点格式精确表示小数。重点是避免 10 次加法和 1 次乘法以提高精度。

标签: floating-point precision floating-accuracy


【解决方案1】:

首先,一些吹毛求疵:

当浮点数不能精确表示时

没有“无法精确表示的浮点数”。所有floats 都可以精确表示为floats。

被多次添加到自身,答案比我们要低 数学上的期望,

当您多次将一个数字添加到自身时,您实际上可以得到比您预期的更高的东西。我将使用C99 hexfloat notation。考虑f = 0x1.000006p+0f。然后f+f = 0x1.000006p+1ff+f+f = 0x1.800008p+1ff+f+f+f = 0x1.000006p+2ff+f+f+f+f = 0x1.400008p+2ff+f+f+f+f+f = 0x1.80000ap+2ff+f+f+f+f+f+f = 0x1.c0000cp+2f。但是,7.0*f = 0x1.c0000a8p+2,四舍五入为0x1.c0000ap+2f,小于f+f+f+f+f+f+f

但是当同一个浮点数乘以一个整数时,答案是, 精确到正确的数字。

7 * 0x1.000006p+0f 不能表示为 IEEE float。因此它被四舍五入。使用默认的舍入模式,即round-to-nearest-with-ties-going-to-even,当您执行这样的单个算术运算时,您将获得最接近精确结果的浮点数。

不过,我不明白的是为什么我们会得到 10.0 对于第一个例子。从数学上讲,我们会 得到 10.0,但知道浮点数是如何存储在内存中的,它不会 对我有意义。谁能解释为什么我们得到一个精确和准确的 通过将不精确的浮点数与 int 相乘来获得值?

要回答您的问题,您会得到不同的结果,因为您执行了不同的操作。你在这里得到了“正确”的答案有点侥幸。

让我们换个号码。如果我计算0x1.800002p+0f / 3,我得到0x1.00000155555...p-1,它四舍五入为0x1.000002p-1f。当我把它翻三倍时,我得到0x1.800003p+0f,它轮到(因为我们打破平局)到0x1.800004p+0f。这与我在float 算术中计算f+f+f 得到的结果相同,其中f = 0x1.000002p-1f

【讨论】:

    【解决方案2】:

    因为1.0 + ((10.0 - 1.0) / 10.0) * 10.0 只对不精确的值进行 1 次计算,因此有 1 个舍入误差,它比对 0.9f 的浮点表示法进行 10 次相加更准确。我认为这是本例中要教授的主要内容。

    关键问题是 0.1 不能用浮点数精确表示。 所以 0.9 里面有错误,会在函数循环中加起来。

    “精确”数字可能会显示出来,因为它有一个聪明的输出格式化例程。当我第一次使用计算机时,他们喜欢将这些数字以一种荒谬的科学固定数字格式输出,这对人类不友好。

    我想要了解发生了什么,我会找到 Koenig 的 Dobbs 博士关于这个主题的博客文章,这是一篇很有启发性的读物,该系列通过展示像 perl、python 和可能的 java 这样的语言如何使计算看起来精确,如果它们是足够精确。

    Koenig's Dr Dobbs article on floating point

    Even Simple Floating-Point Output Is Complicated

    如果 5 到 10 年后将定点算术添加到 CPU 中,请不要太惊讶,金融人士喜欢精确的总和。

    【讨论】:

    • 绝对是一个有用的答案,可以解释这里发生了什么。但是,width~0.9 的值)乘以 10,而不是 min1.0 的值)。尽管如此,您链接到的博客文章给我留下了一个有趣的想法。当width 与自身相加10 次时,不会发生反规范化,因为width 的指数显然是相同的。然后,当将该结果添加到min 时,它足够大以至于在那里也不会发生非规范化。因此,没有任何精度损失,因此宽度的值是“足够精确”可以被认为是精确的。
    • 编译器可以将表达式简化为写掉。你有一个除数,然后是一个乘法。类似地,min + max - min,可以简化为 float p = max; 编译器现在很聪明。
    • 硬件将数字打乱以缩放它们,正如 Amit 指出的那样,聪明的编译器可以检测到您乘以除以的相同值。编译器不想在运行时进行 10 到 10.0f 的昂贵转换。因此,要测试该理论,您需要在运行时输入计数作为浮点数。它应该比 10 个加法更准确,但不能在编译时减少到 float p = max;。感谢您在答案中打勾,您必须经常赶时间才能先进入,然后改进答案,否则您会在编写时发现其他人重复。
    • 啊,这实际上是一个非常好的观点。我没有考虑过这样一个事实,即在计算 p 时,我们实际上最终得到了 min + ((max - min)/count) * count(正如您所指出的,它简化为 p = max)。现在这似乎很明显,我不敢相信我忽略了 xD 感谢您指出这一点。
    • 整个讨论是对我在最初评论中所写内容的冗长而冗长的重复(首先,因为您已经提到了重复)。您的回答虽然内容丰富,但与问题无关。我解释了如何在我的原始评论中验证这一点。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-12-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-05
    • 2014-01-30
    相关资源
    最近更新 更多