是否允许这种浮点优化?

gez*_*eza 88 c++ floating-point clang

我试图检查哪里float失去了精确表示大整数的能力。所以我写了这个小片段:

int main() {
    for (int i=0; ; i++) {
        if ((float)i!=i) {
            return i;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

该代码似乎适用于所有编译器,但不包括clang。Clang生成一个简单的无限循环。上帝保佑

可以吗?如果是,那是QoI问题吗?

Rei*_*ica 63

请注意,内置运算符!=要求其操作数具有相同的类型,并在必要时使用提升和转换来实现。换句话说,您的情况等同于:

(float)i != (float)i
Run Code Online (Sandbox Code Playgroud)

那将永远不会失败,因此代码最终将溢出i,为您的程序提供未定义的行为。因此,任何行为都是可能的。

要正确检查您要检查的内容,您应该将结果投射回int

if ((int)(float)i != i)
Run Code Online (Sandbox Code Playgroud)

  • @Džuris是UB。没有确定的结果。编译器可能意识到,它只能以UB结尾,并决定完全删除循环。 (8认同)
  • @NicHartley:您是说`(int)(float)i!= i`是UB吗?您如何得出结论?是的,它取决于*实现定义的*属性(因为“ float”不需要是IEEE754 binary32),但是在任何给定的实现中,它都是明确定义的,除非“ float”可以精确地表示所有正的“ int”值,所以我们进行了签名-整数溢出UB。(https://en.cppreference.com/w/cpp/types/climits定义了“ FLT_RADIX”和“ FLT_MANT_DIG”来确定)。在一般的打印实现定义的东西中,例如`std :: cout << sizeof(int)`不是UB ... (6认同)
  • @opa是指`static_cast <int>(static_cast <float>(i))`?`reinterpret_cast`很明显是UB (4认同)
  • @Caleth:`reinterpret_cast <int>(float)`并不完全是UB,它只是语法错误/格式错误。如果该语法允许将float类型转换为int来替代memcpy(定义明确),那会很好,但是我认为rerelate_cast <>仅适用于指针类型。 (2认同)
  • @Peter对于NaN,`x!= x`是正确的。[请参见coliru上的直播](http://coliru.stacked-crooked.com/a/e36c8cbc7095e9b4)。在C中也是如此。 (2认同)
  • @Deduplicator:是的,谢谢,我在写答案时从先前的评论中修复了该错误。我使用编译时常数NAN在Godbolt上进行了测试。我希望可以编辑旧评论:PI认为无序意味着任何谓词都是错误的,但是显然!! =的工作方式类似于`!(x == x)而不是其自己的肯定断言。 (2认同)

Pet*_*des 47

正如@Angew指出的那样!=运算符在两侧都需要相同的类型。 (float)i != i也导致RHS浮动(float)i != (float)i


g ++还会生成一个无限循环,但它并不能从内部优化工作。您可以看到它可以将int-> float转换为cvtsi2ss,并且ucomiss xmm0,xmm0可以(float)i与自身进行比较。(这是您的第一个线索,即您的C ++源代码并不表示您认为它像@Angew的答案所说明的那样。)

x != x仅当它是“无序”时才是正确的,因为x是NaN。(INFINITY在IEEE数学中与自己比较,但NaN不相等。 NAN == NAN是false,NAN != NAN是true)。

gcc7.4及更早版本正确地将代码优化jnp为循环分支(https://godbolt.org/z/fyOhW1):只要操作数x != x 不是NaN ,就保持循环。(gcc8及更高版本还会检查是否je中断循环,无法基于对任何非NaN输入始终为真的事实进行优化)。x86 FP比较无序设置的PF。


而且,顺便说一句,这意味着clang的优化也是安全的:CSE必须与CSE (float)i != (implicit conversion to float)i相同,并且证明i -> float在可能的范围内绝不是NaN int

(尽管考虑到此循环将遇到有符号溢出的UB,但ud2无论它的实际循环体是什么,都可以发出它想要的任何asm,包括非法指令或空的无限循环。)但是忽略有符号溢出的UB。 ,此优化仍然是100%合法的。


GCC无法优化循环体,甚至-fwrapv无法使有符号整数溢出得到明确定义(作为2的补码环绕)。 https://godbolt.org/z/t9A8t_

即使启用-fno-trapping-math也无济于事。(不幸的是,
-ftrapping-math即使GCC的实现是损坏的, GCC的默认值也是启用。)int-> float转换可能会导致FP不精确的异常(对于太大而无法准确表示的数字),因此有可能不加掩饰,这是合理的优化环体。(因为16777217如果不精确的异常未被屏蔽,则转换为浮点型可能会有明显的副作用。)

但是,使用时-O3 -fwrapv -fno-trapping-math,有100%的优化未将其编译为空的无限循环。如果不使用#pragma STDC FENV_ACCESS ON,则记录被屏蔽的FP异常的粘性标志的状态不是该代码可观察到的副作用。否int-> float转换会导致NaN,所以x != x不正确。


这些编译器都针对使用IEEE 754单精度(binary32)float和32位的C ++实现进行了优化int

bugfixed(int)(float)i != i循环将有UB对C ++实现窄16位int和/或更广泛的float,因为你会打签署整数溢出UB到达第一个整数,这不是一个精确表示之前float

但是,使用x86-64 System V ABI编译gcc或clang之类的实现时,UB在不同的实现定义选择集下不会产生任何负面影响。


顺便说一句,您可以从FLT_RADIXFLT_MANT_DIG中定义静态计算此循环的结果<climits>。或者至少在理论上,如果float实际上适合IEEE浮点数的模型,而不是像Posit / unum这样的其他实数表示形式,则至少可以。

我不确定ISO C ++标准对float行为有多大的规定,以及不基于固定宽度指数和有效字段的格式是否符合标准。


在评论中:

@geza我想听听得到的数字!

@nada:是16777216

您是否声称要让此循环打印/返回16777216

更新:由于该评论已被删除,我认为没有。可能OP只是float在不能完全表示为32位的第一个整数之前引用floathttps://zh.wikipedia.org/wiki/Single-precision_floating-point_format#Precision_limits_on_integer_values, 即他们希望通过此错误代码进行验证的内容。

错误修复的版本当然会打印出16777217,第一个不能精确表示的整数,而不是之前的值。

(所有较高的float值都是精确的整数,但是对于大于有效宽度的指数值,它们是2,4,8的倍数。可以表示许多较高的整数值,但最后一个单位(有效数的)大于1,所以它们不是连续的整数。最大的有限float值刚好在2 ^ 128以下,对于偶数而言太大int64_t。)

如果有任何编译器确实退出了原始循环并打印出来,那将是编译器错误。

  • @SombreroChicken:不,我首先学习了电子学(从我父亲身边读过的一些教科书中;他是物理学教授),然后学习了数字逻辑,然后进入了CPU /软件。:P如此,我一直很喜欢从头开始理解事物,或者,如果我从更高的层次开始,那么我想至少学习一些低于该层次的知识,这些知识会影响事物在我这个层次中的工作方式/为什么在思考。(例如,asm的工作方式以及如何对其进行优化受CPU设计约束/ cpu体系结构的影响。而这又来自物理+数学。) (3认同)