IEEE 754-2008 是确定性的吗?

Joa*_*ner 6 javascript floating-point deterministic ieee-754

如果我从相同的值开始,并在双精度 64 位 IEEE 754-2008 值上执行相同的原始操作(加法、乘法、比较等),我会得到相同的结果,独立于底层机器吗?

更具体地说:由于ECMAScript 2015指定数字值是

对应于双精度 64 位二进制格式 IEEE 754-2008 值的原始值

我可以得出结论,相同的操作在这里产生相同的结果,独立于环境吗?

Flo*_*dit 7

(这里有很多脚注是为了避开实际上的人群,但它们不会影响您关于 ECMAScript 的问题。)

\n

IEEE 754

\n
\n

如果我从相同的值开始,并对双精度 64 位 IEEE 754-2008 值执行相同的原始操作(加法、乘法、比较等),我会得到相同的结果,而与底层机器无关吗?

\n
\n

是的。

\n

IEEE 754-2008(和 IEEE 754-2019)标准精确定义了所有浮点值的加法、减法、乘法、除法和平方根运算,不同 NaN 值之间的区别除外。1 \n标准2的实现在所有输入上达成一致。\n这同样适用于三向比较(<、= 或 >,在数字上定义,包括无穷大;在 NaN 上引发异常)或四向比较(<、= 、> 或无序,在所有浮点值(包括 NaN)上定义。

\n

这五个算术运算不仅在所有输入上都精确定义,而且对于数字输入,它们也被精确定义为正确舍入:浮点加法运算 \xe2\x8a\x95 定义为给出 fl( + ),即实数总和的舍入结果 + 根据当前舍入模式,3默认返回最接近的浮点数,或者,如果出现平局,则返回最低有效位为偶数的最接近的浮点数。

\n

ECMAScript 2015(和 2021)

\n
\n

更具体地说:由于ECMAScript 2015指定数字值是

\n
\n

对应于双精度 64 位二进制\n格式 IEEE 754-2008 值的原始值

\n
\n

我是否可以得出结论,相同的操作会产生相同的结果,而与环境无关?

\n
\n

是的。

\n

ECMAScript 2015 中的数字运算+、-、*、 和/都是在所有输入上精确定义的,与 IEEE 754 一致。4 例如,ECMAScript 2015 中加法的定义具体指出:

\n
\n

加法的结果使用 IEEE 754-2008 二进制双精度算术规则确定:

\n
\n

ECMAScript 2021 中的加法定义基本保持不变,更新为引用 IEEE 754-2019:

\n
\n

抽象操作 Number::add 接受参数 x(一个数字)和 y(一个数字)。它根据 IEEE 754-2019 二进制双精度算术规则执行加法,生成其参数的总和。

\n
\n

同样,ECMAScript 2015 中的相等性和ECMAScript 2021 中的相等性的定义与 IEEE 754-2008 和 IEEE 754-2019 一致,尽管没有明确引用。 ECMAScript 2015 中的关系运算符和ECMAScript 2021 中的关系运算符都实现了 IEEE 754 有序比较概念,false当任一输入为 NaN 时返回,否则返回适当的顺序。

\n

Math.sqrt在 ECMAScript 2015和Math.sqrtECMAScript 2021 中,允许将实现定义的近似值(受到极端情况的约束)返回到平方根,即使 IEEE 754 精确定义了平方根运算并且从 IEEE 开始就这样做了754-1985。\n但实际上,实现极不可能无法按照 IEEE 754 的要求返回正确的舍入结果。

\n

注意:除了四或五个基本算术运算 ( +, -, *, /; ) 之外的许多运算Math.sqrt都允许,并且很可能会因实现而异。\n例如,一种实现可能使用 的简单多项式近似Math.log1p,而另一种实现可能使用 的简单多项式近似可能会使用一组表驱动的近似值,从而为某些输入提供略有不同的结果。\n这有时被用作浏览器指纹识别的向量。\n但是您仅使用基本算术运算实现的任何近似值在所有 ECMAScript 实现中都会一致。

\n

%ECMAScript 2015和%ECMAScript 2021 中的运算符针对所有输入进行了精确定义,但与 IEEE 754 余数运算不一致:ECMAScript%使用截断除法,而 IEEE 754 余数使用舍入到最近/偶数除法。 \n(ECMAScript%采用fmodC 语言,而 IEEE 754 余数remainder采用 C 语言。)

\n

其他语言

\n

上述答案并不总是适用于其他语言。\n例如,绝大多数 C 实现为 提供 IEEE 754 二进制 64 算术double和 为 二进制 32 算术,但 C 标准允许它们在表达式 中float使用不同的算术规则,前提是它们指定通过宏的规则是什么:FLT_EVAL_METHOD

\n
\n

除了赋值和转换(删除所有额外的范围和精度)之外,具有浮点操作数的运算符生成的值以及经过通常算术转换和浮点常量的值将被评估为其范围和精度可能大于所要求的格式类型。\n评估格式的使用以实现定义的值为特征FLT_EVAL_METHOD:

\n
    \n
  • -1无法确定的;
  • \n
  • 0仅根据类型的范围和精度评估所有操作和常量;
  • \n
  • 1计算类型的操作和常量float以及double类型的范围和精度double,计算类型long double的操作和常量的范围和精度long double;
  • \n
  • 2将所有操作和常量评估为类型的范围和精度long double。
  • \n
\n

所有其他负值都FLT_EVAL_METHOD表示实现定义的行为。

\n
\n

(C11,\xc2\xa75.2.4.2.2:浮点类型的特征<float.h>,\xc2\xb69,第30页)

\n

这意味着当一个实现定义为 时FLT_EVAL_METHOD,2像这样的函数

\n
double\nnaive_fma(double x, double y, double z)\n{\n    return x*y + z;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

将像已编写的那样执行:

\n
double\nnaive_fma(double x, double y, double z)\n{\n    return (long double)x*z + z;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

Intel IA-32 架构上的 C 实现 (\xe2\x80\x9ci386\xe2\x80\x9d) 通常以这种方式工作:它们使用 Intel x87浮点单元来计算 80 位二进制浮点算术中的表达式具有 64 位精度(\xe2\x80\x9c双扩展精度\xe2\x80\x9d),然后舍入为 IEEE 754 二进制 64(无论结果存储在变量中、double作为double、 参数传递或显式转换为double)。5

\n

然而,这种计算表达式的方法在 ECMAScript 中是不允许的,因此您不必担心它。\n通过编译为 ECMAScript 的 C 实现,显而易见的方式将简单地定义FLT_EVAL_METHOD为0。

\n
\n

1 \nNaN 有效负载的内容可能因实现而异。\n但是,结果是否为 NaN,以及 NaN 结果是信令还是静默,均由标准定义。\n

\n

2 \n某些硬件还提供非标准操作模式,例如清零,这会导致操作在 IEEE 754 语义下返回非正常数字时返回零;在这种情况下,硬件不是标准的实现。\n如果启用这些模式,那么您可能会得到不同的答案,但通常它们不会启用,并且它们违反了数值算法通常假定的定理,例如 Sterbenz 引理,因此它们仅在专门的应用程序中使用。\nECMAScript 不支持刷新到零或其他非标准操作模式,也不支持我所知道的任何实现:您可以依赖于 IEEE 754 中定义的逐渐下溢到次正常值。\n

\n

3 \nIEEE 754 允许实现保持动态舍入模式,定义了四个舍入方向:最接近/偶数、向上(向正无穷大)、向下(向负无穷大)和向零。\n在某些环境中,程序可以查询和更改当前的舍入模式,例如在 C 中使用 和fegetround,fesetround尽管对此的工具链支持通常是有限的,并且它主要用于向数值算法中注入小扰动,以检查输出中指示问题的剧烈变化算法。\nECMAScript 不支持更改舍入模式,我所知道的任何实现也不支持更改舍入模式:您只需处理默认的舍入到最近/联系到偶数。\n

\n

4 \nECMAScript 的语义仅区分单个 NaN 值;ECMAScript 中没有 NaN 有效负载或信令与安静 NaN 的概念。\n在幕后,两个 NaN 可能以不同的位模式存储,但 ECMAScript 不会在语义上区分它们,也没有提供区分它们或检查它们的方法幕后的位模式。\n

\n

5 \n以更高精度计算表达式有时会导致双舍入错误\xe2\x80\x94,例如,添加 0x1p+53 和 0x1.7ffp+1,第一次舍入到 64 位精度将得到 0x1。 000000000000018p+53,因此第二次舍入到 53 位精度给出 0x1.00000000000002p+53,而 53 位精度的正确舍入总和是 0x1.00000000000001p+53。\n那么为什么要这样做?\n在实践中,它几乎总是领先通过使用更高的中间精度,可以提高数值算法的准确性:您可以承受损失数千个 64 位精度的 ulp,但仍然获得在 53 位精度的几个 ulps 以内的答案。\n

\n