在C++中,浮点数加法是可交换的吗?

49

对于浮点数而言, a + b 等价于1 b + a 是否是有保证的?

我相信在IEEE754中这是有保证的,但C++标准并没有规定必须使用IEEE754。唯一相关的文本似乎来自[expr.add]#3:

二元+运算符的结果是操作数的总和。

数学运算“总和”是可交换的。然而,数学运算“总和”也是可结合的,而浮点加法明显不是可结合的。因此,我认为我们不能得出“总和”在数学上具有交换性意味着该引用指定了C ++中的交换性。


注脚1:
“同一”的含义是按位相同,类似于memcmp而不是==,以区分+0和-0。 IEEE754将+0.0 == -0.0视为真,但也针对有符号零制定了特定规则。 IEEE754中+0 + -0-0 + +0都产生+0,相等大小反号值的加法也是如此。如果遵循IEEE语义的==将隐藏有符号零的非交换性,如果这是标准。

此外,在IEEE754数学中,如果任何输入为NaN,则a + b == b + a是错误的。 memcmp将说两个NaN是否具有相同的位模式(包括有效负载),尽管我们可以将NaN传播规则单独考虑,而不涉及有效的数学运算的交换性。


4
std::strtod("nan")+0.0 == 0.0+std::strtod("nan") 的结果为 false。但我怀疑这不是你想要表达的意思。 - aschepler
2
为什么要局限于浮点数?没有经过太多思考,我认为C++也不要求整数加法是可交换的(例如,溢出结果可能是非交换的)。 - Stephen Canon
4
这似乎取决于你想要给未定义行为的处理方式。对于不引发未定义行为的程序,整数加法是可交换的,而程序是否引发未定义行为不能取决于整数加法的可交换性。 - tmyklebu
1
询问 a+b == b+a 是否相同于询问它是否可交换是不同的,因为 ==memcmp 不同。在类似 IEEE754 的系统中,最可能的非交换性,比如一些软件浮点数模拟器,可能是由于带符号零或不同的 NaN 负载。(例如,0 + -0 可能总是采用左操作数的符号,而在 IEEE754 中总是 +0)。但它仍然可能实现 -0 == +0 的 IEEE 语义。(此外,如果 a 或 b 是 NaN,则对于 IEEE754,a+b == b+a 为 false。再次提醒,memcmp 实际上会检查可交换性)。 - Peter Cordes
1
对于内置类型,gcc将在没有任何特殊预防措施的情况下交换+的操作数。 - Marc Glisse
我了解C++符合IEEE标准(对于非NAN的加法是可交换的),除非您使用--fast-math-Ofast,在这种情况下可能会发生任何事情。 - alfC
4个回答

24

甚至并不需要a + b == a + b。子表达式中的一个可能具有比另一个更高的精度,例如当使用多个加法时,需要将其中一个子表达式暂时存储在内存中,而另一个子表达式可以保留在寄存器中(具有更高的精度)。

如果不能保证a + b == a + b,则不能保证a + b == b + a。 如果a + b不必每次返回相同的值,并且这些值不同,则其中一个必然不等于b + a 的某个特定评估。


1
@MattMcNabb 这取决于编译器想要遵循哪些规则。C和C++标准非常宽松,但由于它们如此宽松,实现可能会实施更严格的规则(可能符合其他标准),并仍然符合C++标准。这样更严格的规则确实会禁止许多浮点数优化。C++标准在[expr.prim.general]p11中规定:“浮点操作数的值和浮点表达式的结果可以用比类型所需更大的精度和范围表示;因此不改变类型。” - user743382
1
@MattMcNabb 实际上意味着,例如,如果 abdouble 类型,并且 a + blong double 中可以精确表示,但在 double 中需要四舍五入,而 CPU 只有一个具有 long double 精度的浮点类型,则可能会得到 (long double) a + (long double) b 的结果。C 和 C++ 标准都故意不想要求编译器在每个浮点操作之后发出舍入指令。 - user743382
5
有一个GCC的[bug 323](https://gcc.gnu.org/bugzilla/show_bug.cgi?id=323),涉及一个程序假设 a + b == a + b,然而GCC开发人员已将其关闭,认为这不是一个bug。请注意,本翻译仅供参考,不保证完全准确无误。 - user743382
3
比起a+b == a+b不能保证成立,更糟糕的是IEEE-754标准要求一些a的值甚至不满足a==a - supercat
3
@KamiKaze:我指的是NaN。虽然可以编写一个NaN安全的浮点排序,通过使用初始传递将数据分成NaN和其他所有内容,并对不包含任何NaN值的部分进行排序,从而表现得合理,但这是一种可以通过定义一种一致工作的关系运算符来避免的额外复杂性。 - supercat
显示剩余11条评论

22

不,C++语言通常不会对硬件有这样的要求,只定义了运算符的结合性。

在浮点算术中可能会发生各种疯狂的事情。也许在某些机器上,将零添加到非规格化数会产生零。可能一台机器可以避免在向内存中的非规格化数添加零值寄存器时更新内存。还有可能一个非常愚蠢的编译器总是会把左操作数放在内存中而右操作数放在寄存器中。

请注意,如果一个机器具有非交换加法,则需要明确地定义表达式如何映射到指令,如果您要控制获得哪个操作。左操作数进入第一个机器操作数还是第二个?

这样的ABI规范,在同一句话中提到表达式和指令的构造,会非常病态。


14
运算符结合性与数学中的乘法结合律不同。前者指的是double x = a * b * c;将以从左至右的方式计算为(a * b) * c。在数学上,结合律意味着(a * b) * c == a * (b * c)。浮点数加法/乘法采用从左至右的方式计算,但明显不是数学上的结合性(即从右至左的计算可能会产生不同的结果) 。 - TemplateRex
@TemplateRex 嗯,我不明白那与问题或我的回答有什么关系。这个讨论完全是关于用于编程浮点计算的符号表示法,而不是被近似的数学运算。 - Potatoswatter
9
我只是指出了结合律有两个不同的含义,并且从上下文中并不清楚你指的是哪一个。 - TemplateRex

12

C++标准非常明确地不保证IEEE 754。尽管库对IEC 559(基本上就是IEC版本的IEEE 754标准)提供了一些支持,因此您可以检查底层实现是否使用IEEE 754 / IEC 559(当它确实如此时,您当然可以依赖于它所保证的)。

在大多数情况下,C和C ++标准假定这些基本操作将以底层硬件的方式实现。 对于像IEEE 754这样常见的东西,它们会让您检测其是否存在,但仍然不需要它。


我已经接受了hvd的答案(至少暂时是这样)- 假设它是正确的,那么它表明即使在IEEE754实现中,如果结果不能精确表示,那么我们甚至没有恒等性,更不用说交换律了。 - M.M

2
在任何给定精度下,除了 NaN 负载之外,使用 IEEE FP 数学的 C++ 实现的加法是可交换的。
Marc Glisse 评论道:
对于内建类型,gcc 将不带任何特别预防地交换 + 的操作数。
  • 有限输入且结果非零的情况是简单的情况,显然是可交换的。加法是"基本"浮点数运算之一,因此IEEE754要求结果是"正确舍入"(舍入误差<= 0.5ulp),因此只有一个可能的数字结果,以及仅一种代表它的位模式。

    非IEEE浮点数运算可能允许更大的舍入误差(例如允许在尾数最低有效位上错开一个单位,因此舍入误差<= 1 ulp)。这可能导致非可交换性,最终结果取决于操作数的顺序。我认为大多数人会认为这是一个糟糕的设计,但C++可能没有禁止它。

  • 如果结果为零(具有相同大小但符号相反的有限输入),则在IEEE数学中始终为+0.0。(或在向负无穷方向舍入模式中为-0.0)。该规则涵盖了产生+0.0+0+(-0.0)和反之产生相同结果的情况。请参阅What is (+0)+(-0) by IEEE floating point standard?

    除非您在以清零模式运行的FPU中具有次正常输入(次正常输出向零舍入),否则具有不同大小的输入不能下溢为零。在这种情况下,如果确切结果为负,则可能会得到-0.0作为结果。但它仍然是可交换的。

  • 加法可以从-0 + -0产生-0.0,因为两个输入都是相同的值,所以是平凡的可交换的。

  • -Inf + 任何有限值是-Inf,+ Inf + 任何有限值是+ Inf,+ Inf + -Inf是NaN。这两者都不取决于顺序。

  • NaN + 任何值或任何值 + NaN均为NaN。NaN的"有效载荷"(尾数)取决于FPU。我IRC,保留上一个NaN的有效载荷。

  • NaN + NaN产生NaN。如果我记得正确,没有规定保留哪个NaN有效载荷,或者是否可以发明新的有效载荷。几乎没有人使用NaN有效载荷来跟踪它们来自何处,因此这并不重要。

C++ 中的 + 运算符会将两个输入值提升为匹配类型,如果它们尚未匹配,则提升为两个输入类型中更宽的那个。因此,类型不对称。


对于单独的 a+b == b+a,由于 IEEE 的 == 语义(而不是 + 语义)的原因,它可能对 NaNs 不成立,就像 a+b == a+b 一样。

使用严格的 FP 数学(在 C 语句之间没有额外的精度保留,例如,在 x86 上使用传统的 x87 数学时,gcc -ffloat-store),我认为相等性等价于 !isunordered(a,b),它测试它们中的任何一个是否为 NaN

否则,编译器可能会与一个代码进行CSE,而另一个却不会,并且其中一个将使用更高精度的ab值进行评估。(严格的ISO C++要求高精度临时变量仅存在于表达式中,即使是FLT_EVAL_METHOD==2 (如x87),也不能跨语句,但默认情况下gcc不遵守该规定。只有使用g++ -std=c++03或类似于gnu++20的选项,或者特别针对x87使用-ffloat-store才能遵守该规定。)
在C++实现中,如果FLT_EVAL_METHOD == 0(表达式中没有额外的临时变量精度),这种优化差异的来源就不会成为一个因素。

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