为什么 Java 的 double/float Math.min() 是这样实现的?

Yon*_*har 70 java floating-point

我正在查看 的源代码中的一些内容java.lang.Math,我注意到 while Math.min(int, int)(或其长版本)是这样实现的:

public static int min(int a, int b) {
   return a <= b ? a : b;
}
Run Code Online (Sandbox Code Playgroud)

这对我来说完全有意义,这和我要做的一样。但是,双/浮点实现是这样的:

public static float min(float a, float b) {
   if (a != a) {
      return a;
   } else if (a == 0.0F && b == 0.0F && (long)Float.floatToRawIntBits(b) == negativeZeroFloatBits) {
      return b;
   } else {
      return a <= b ? a : b;
   }
}
Run Code Online (Sandbox Code Playgroud)

我完全傻眼了。相比a于自己?第二次检查是为了什么?为什么它的实现方式与 int/long 版本不同?

Joa*_*uer 103

浮点数比整数值复杂得多。

对于这种特定情况,有两个区别很重要:

  • NaN是 和 的有效值floatdouble它代表“不是数字”并且表现得很奇怪。也就是说,它不等于它自己。
  • 浮点数可以区分 0.0 和 -0.0。当您计算某个函数的极限时,负零可能很有用。区分极限是从正方向还是从负方向接近 0 可能是有益的。

所以这部分:

if (a != a) {
      return a;
}
Run Code Online (Sandbox Code Playgroud)

确保NaN如果aNaN(如果a不是NaN,但是b是,则稍后的“正常”检查将返回b,即NaN,因此在这种情况下不需要显式检查)。这是一种常见的模式:当计算任何输入为 时NaN,输出也将为NaN。由于NaN通常表示计算中的一些错误(例如 0 除以 0),因此它“毒化”所有进一步的计算以确保错误不会被默默吞下是很重要的。

这部分:

if (a == 0.0F && b == 0.0F && (long)Float.floatToRawIntBits(b) == negativeZeroFloatBits) {
      return b;
}
Run Code Online (Sandbox Code Playgroud)

确保如果您比较两个零值浮点数并且b是负零,则返回负零(因为 -0.0 “小于”0.0)。与NaN正常检查类似,a如果它是 -0.0 并且b是 0.0,则会正确返回。

  • 可能要注意,如果“b”是 NaN,则最后一个分支中条件的确切形状(“a &lt;= b ? a : b”)很重要。`b` 必须位于假值一侧才能返回 NaN。类似地,如果“a = -0”和“b = +0”,则最后一个条件比较相等并返回“a”。两者一起意味着只需要这两个显式测试,而不是完整的四个测试。 (14认同)
  • +1但挑剔:“计算中出现一些错误(例如将值除以0)”。0/0 是 NaN,其他数字的 x/0 通常是 +/- Infinity。 (13认同)
  • @EricDuminil:是的,正如 ikkachu 指出的那样,这有点过于简单化了。我来编辑。 (2认同)
  • 我认为 `a == 0.0F &amp;&amp; b == 0.0F` 可以优化为 `a == b`。也许这个想法是“Float.floatToRawIntBits”很昂贵,所以他们试图在更多情况下将其短路?那么,也许 `a==b &amp;&amp; b == 0.0F` 可以避免在常见的不等于情况下需要常量。如果`==`为假,那么在像x86这样的机器上,当达到三元数时,FLAGS将根据`a比较b`设置。(如果您要为此使用无分支“minss”指令,这没有帮助......) (2认同)

Swe*_*per 40

我建议仔细阅读有关浮点数Math.min数字比较运算符的文档。他们的行为完全不同。

相关部分来自Math.min

如果任一值为 NaN,则结果为 NaN。与数值比较运算符不同,此方法认为负零严格小于正零。

来自 JLS §15.20.1“数值比较运算符 <、<=、> 和 >=”

由 IEEE 754 标准的规范确定的浮点比较的结果是:

  • 如果任一操作数为 NaN,则结果为假。

  • 正零和负零被认为是相等的。

如果任何参数为 NaN,则Math.min选择该参数,但如果任何操作数为 NaN,则<=计算结果为false。这就是为什么它必须检查是否a不等于自身 - 这意味着a是 NaN。如果a不是 NaN 而是 NaN b,则最后一种情况将涵盖它。

Math.min也认为-0.0是 "less than" +0.0,但数字比较运算符认为它们相等。这是第二次检查的目的。


Shr*_*saR 18

为了完整性/清晰性,让我们绘制一个所有可能结果的表格:

  • 无论的ab可以是

    • ,
    • ?0 ,
    • 0(即+0),或
    • 其他一些非 NaN 非零值,标记为“(其他)”。

    为了完整起见,写出这些的所有组合,并在某些情况下为清楚起见区分正数和负数,在下表中给出了 20 行,尽管它们中的大多数都很简单且没有问题。

  • 题为“正确分钟”一栏是应该根据返回的正确值 IEEE 754标准 和Java文档Math.min,以及名为“天真分钟”栏目是,本来如果返回的值Math.min已经实现return a <= b ? a : b;,而不是.

一种 正确的分钟 天真的分钟 naive min 的注意事项 天真敏错了吗?
NaN NaN NaN NaN b,因为 NaN 比较给出错误。
NaN ?0 NaN ?0 b,因为 NaN 比较给出错误。 错误的
NaN 0 NaN 0 b,因为 NaN 比较给出错误。 错误的
NaN (其他) NaN (其他) b,因为 NaN 比较给出错误。 错误的
?0 NaN NaN NaN b,因为 NaN 比较给出错误。
?0 ?0 ?0 ?0 a, 作为 ?0 ? ?0。
?0 0 ?0 ?0 a, 作为 ?0 ? 0.
?0 (其他>0) ?0 ?0
?0 (其他<0) (其他<0) (其他<0)
0 NaN NaN NaN b,因为 NaN 比较给出错误。
0 ?0 ?0 0 a,按照 IEEE 754 为“0 ? ?0”。 错误的
0 0 0 0 a, 作为 0 ? 0.
0 (其他>0) 0 0
0 (其他<0) (其他<0) (其他<0)
(其他) NaN NaN NaN b,因为 NaN 比较给出错误。
(其他<0) ?0 (其他<0) (其他<0)
(其他>0) ?0 ?0 ?0
(其他<0) 0 (其他<0) (其他<0)
(其他>0) 0 0 0
(其他) (其他) (其他) (其他)

[“Correct min”和“Naive min”的最后一行中的“(other)”表示正确的最小值,直截了当,不会因为 NaN 或 ?0 而混淆。]

所以你会看到上表中有四行,naive 函数会给出错误的答案:

  • 其中三个a是 NaN时的情况,但b不是。这就是函数中的第一个检查的目的。

  • 另一种情况Math.min(0, -0)是 Java 记录为返回 ?0 的情况,即使 IEEE 754 将 0 和 ?0 视为相等进行比较(因此比较“0 ? ?0”评估为真)。这就是函数中的第二个检查的目的。


Mat*_*ias 11

我可以帮你第一次比较if (a != a)。这显然只看a,那么在哪些情况下可能a是最小值而不管b

float数不同于int由具有特殊值,例如NAN。的一个特殊属性NAN是比较总是错误的。因此,a如果每个比较运算符在 上都返回 false,则第一个条件返回a

b在最后一行可以找到相同的条件。如果对上的比较b始终返回 false,则最后一行始终返回b

在第二个条件下,我只能猜测这与“负零”和“正零”有关,另外两个特殊值float. 当然,负零小于正零。

  • 在其他人出现之前我就开始写答案了。所以我在发布答案后才看到他们。这就是这个答案被否决的原因吗? (3认同)
  • 有些人似乎拒绝投票迟到或含糊的答案,至少这是我的经验 (3认同)
  • 为什么两个答案在没有猜测的情况下提供了相同的信息后,您却输入了猜测的答案? (2认同)
  • 感谢您提到如何检查 b 是否为 NaN。 (2认同)