IEEE 754-2008是否是确定性的?

8
如果我使用相同的值,并在双精度64位IEEE 754-2008值上执行相同的原始操作(加法、乘法、比较等),那么我会得到相同的结果,与底层机器无关吗?
更具体地说:由于 ECMAScript 2015 指定数字值是
原始值对应于双精度64位二进制格式IEEE 754-2008值
我可以得出结论,在此处进行相同的操作将产生相同的结果,与环境无关?

请注意,此问题与可能成立或不成立的代数恒等式无关。 - Joachim Breitner
以下是一些可能对本问题读者感兴趣的相关链接:https://randomascii.wordpress.com/2013/07/16/floating-point-determinism/ - Joachim Breitner
可能相关的问题:https://dev59.com/52kv5IYBdhLWcg3wzj_a#10338202 - Joachim Breitner
我也对这个问题感兴趣,但我想指出现在的替代方案可能是使用某些本地语言(例如C或Rust)中的定点算术 + WebAssembly,并向JavaScript端暴露API。将每个运算符公开为一个函数,该函数操作包含您的定点表示的“TypedArray”。这是跨每个可想象平台100%保证确定性。 - Ivan Perevezentsev
1个回答

6

(这里有很多脚注来防止那些喜欢挑剔的人,但它们不会影响您对ECMAScript的问题。)

IEEE 754

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

是的。

IEEE 754-2008(和IEEE 754-2019)标准精确地定义了所有浮点值上的加法,减法,乘法,除法和平方根运算,除了不同NaN值之间的区别。1 标准的实现2在所有输入上达成一致。 对于三路比较(<,=或>,定义在数字上,包括无穷大;在NaN时引发异常)或四路比较(<,=,>或无序,定义在所有浮点值包括NaN上),情况也是如此。

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

ECMAScript 2015(和2021)

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

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

我可以得出结论,在此处执行相同操作将独立于环境产生相同的结果吗?

是的。

在ECMAScript 2015中,数字的+-*/运算都与IEEE 754达成一致,对所有输入都有明确定义。4例如,ECMAScript 2015中的加法的定义明确说明:

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

ECMAScript 2021中的加法的定义基本相同,更新为引用IEEE 754-2019:

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

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

Math.sqrt在ECMAScript 2015中,以及Math.sqrt在ECMAScript 2021中,允许返回一个实现定义的近似值(受到有关边界情况的约束),即使IEEE 754自始至终都精确定义了平方根运算。

实际上,一个实现无法按照IEEE 754要求返回正确舍入的结果的可能性极小。

注意:很多操作除了四五个基本算术运算(+、-、*、/;Math.sqrt)之外,都是允许的,并且很可能会因实现而异。例如,一个实现可能使用简单的多项式逼近来计算Math.log1p,而另一个实现可能使用一组表驱动的逼近,对某些输入给出略微不同的结果。这有时被利用作为浏览器指纹识别的向量。但是,任何你使用基本算术运算实现的逼近在所有ECMAScript实现中都是相同的。

在ECMAScript 2015中,运算符%和ECMAScript 2021中的%对于所有输入都有精确定义,但与IEEE 754余数操作不一致:ECMAScript %使用截断除法,而IEEE 754余数使用最接近/平均分配除法。(在C语言中,ECMAScript的%fmod,而IEEE 754余数是remainder。)

其他语言

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

除了赋值和强制类型转换(它们会移除所有额外的范围和精度),使用浮点操作数的运算符、受常规算术转换影响的值以及浮点常量产生的值将被评估为一种格式,其范围和精度可能大于类型所需。 使用评估格式的特征是 FLT_EVAL_METHOD 的实现定义值:
  • -1 不确定;
  • 0 仅将所有操作和常量评估到类型的范围和精度;
  • 1 将类型为 floatdouble 的操作和常量评估到 double 类型的范围和精度,将类型为 long double 的操作和常量评估到 long double 类型的范围和精度;
  • 2 将所有操作和常量评估到 long double 类型的范围和精度。
FLT_EVAL_METHOD 的所有其他负值都表征着实现定义行为。
这意味着当实现定义 FLT_EVAL_METHOD2 时,像这样的函数:
double
naive_fma(double x, double y, double z)
{
    return x*y + z;
}

将被实现,就好像它已经被编写:

double
naive_fma(double x, double y, double z)
{
    return (long double)x*z + z;
}

在Intel IA-32架构(“i386”)上实现C的方式通常是这样的:它们使用Intel x87浮点单元,以80位二进制浮点算术计算具有64位精度(“双扩展精度”)的表达式,然后在结果存储到double变量、作为double参数传递或显式转换为double时,将其四舍五入到IEEE 754 binary64。5 然而,在ECMAScript中不允许使用这种表达式计算方法,因此您不必担心。按照明显的编译成ECMAScript的方式实现C的实现将简单地将FLT_EVAL_METHOD定义为0

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

2 一些硬件也提供了非标准的操作模式,比如flush-to-zero,当操作根据IEEE 754语义返回次正常数时,会导致操作返回零;在这种情况下,硬件不是标准的实现。 如果启用这些模式,则可能会得到不同的答案,但通常它们没有启用,并且违反了数字算法通常假定的定理,例如Sterbenz引理,因此只用于专门的应用程序。 ECMAScript不支持flush-to-zero或其他非标准的操作模式,我所知道的任何实现都不支持:您可以依赖IEEE 754中定义的渐进下溢到次正常数。

IEEE 754允许实现维护动态舍入模式,定义了四个舍入方向:最近舍入/平均舍入、向上(朝正无穷)、向下(朝负无穷)和朝零。在某些环境中,程序可以查询和更改当前的舍入模式,例如在C中使用fegetroundfesetround,但是对此的工具链支持通常有限,并且它主要用于将小扰动注入数值算法中,以检查输出中的急剧变化,以指示算法中的问题。ECMAScript不支持更改舍入模式,我所知道的任何实现都不支持:您只需要处理默认的最近舍入/平均舍入。
ECMAScript的语义仅区分单个NaN值;在ECMAScript中没有NaN有效载荷或信号与安静NaN的概念。在底层,可能使用不同的位模式存储两个NaN,但是ECMAScript在语义上不区分它们,并且不提供区分它们或检查底层位模式的方法。
评估高精度表达式有时会导致双重舍入错误,例如加上0x1p+53和0x1.7ffp+1,第一次舍入到64位精度将给出0x1.000000000000018p+53,因此第二次舍入到53位精度将给出0x1.00000000000002p+53,而使用53位精度正确舍入的总和是0x1.00000000000001p+53。 那么为什么要这样做呢? 实际上,它几乎总是通过使用更高的中间精度来提高数值算法的准确性:您可以在64位精度下失去成千上万个ulp,仍然可以获得一个在53位精度内的答案。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接