我试图检查float
在哪里失去了精确表示大整数的能力。所以我写了这个小片段:
int main() {
for (int i=0; ; i++) {
if ((float)i!=i) {
return i;
}
}
}
此代码似乎适用于所有编译器(clang除外)。 Clang生成一个简单的无限循环。 Godbolt。
可以吗?如果是,那是QoI问题吗?
答案 0 :(得分:61)
请注意,内置运算符!=
要求其操作数为同一类型,并在必要时使用提升和转换来实现。换句话说,您的情况等同于:
(float)i != (float)i
那应该永远不会失败,因此代码最终将溢出i
,为您的程序提供未定义的行为。因此,任何行为都是可能的。
要正确检查要检查的内容,应将结果回退到int
:
if ((int)(float)i != i)
答案 1 :(得分:39)
As @Angew pointed out,!=
运算符两边都需要相同的类型。
(float)i != i
也会导致RHS浮动,因此我们有 (float)i != (float)i
。
g ++还会生成一个无限循环,但是它并不能优化其中的工作。您可以看到它将cvtsi2ss
转换为int-> float,并执行ucomiss xmm0,xmm0
将(float)i
与自身进行比较。 (这是您的第一个线索,即您的C ++源代码并不表示您认为它像@Angew的答案所说明的那样。)
x != x
仅在“无序”时才为真,因为x
是NaN。 (INFINITY
在IEEE数学中与其自身比较,但NaN不相等。NAN == NAN
为假,NAN != NAN
为真。)
gcc7.4及更早版本正确地将代码优化为jnp
作为循环分支(https://godbolt.org/z/fyOhW1):只要循环到x != x
的操作数不是NaN,就继续循环。 (gcc8和更高版本还会检查je
使其脱离循环,无法基于对任何非NaN输入始终为真的事实进行优化)。 x86 FP比较无序设置的PF。
顺便说一句,顺便说一句,对clang的优化也是安全的:它只需要CSE (float)i != (implicit conversion to float)i
一样,并证明i -> float
绝不可能是NaN范围int
。
(尽管该循环将遇到有符号溢出的UB,但实际上它可以发出它想要的任何asm,包括ud2
非法指令,或者是一个空的无限循环,无论循环主体是什么。 ),但是忽略签名溢出的UB,此优化仍然是100%合法的。
即使使用-fwrapv
,GCC也无法优化循环体,无法使有符号整数溢出得到明确定义(作为2的补码环绕)。 https://godbolt.org/z/t9A8t_
即使启用-fno-trapping-math
也无济于事。 (GCC的默认值为unfortunately以启用
-ftrapping-math
,即使GCC's implementation of it is broken/buggy。)int-> float转换也可能导致FP不精确的异常(对于太大而无法准确表示的数字),因此可能会掩盖异常,因此不优化循环体是合理的。 (因为如果未屏蔽不精确的异常,将16777217
转换为float会产生明显的副作用。)
但是使用-O3 -fwrapv -fno-trapping-math
时,有100%的优化未将其编译为空的无限循环。如果没有#pragma STDC FENV_ACCESS ON
,则记录被屏蔽的FP异常的粘滞标记的状态不是该代码可观察到的副作用。 int
-> float
的转换不会导致NaN,因此x != x
不能成立。
这些编译器都针对使用IEEE 754单精度(binary32)float
和32位int
的C ++实现进行了优化。
修正了(int)(float)i != i
的循环会在窄16位int
和/或更宽float
的C ++实现中使用UB,因为您遇到了签名-integer溢出UB,然后到达第一个不能完全表示为float
的整数。
但是,使用x86-64 System V ABI编译gcc或clang之类的实现时,UB在不同的实现定义的选择集下不会产生任何负面影响。
顺便说一句,您可以根据<climits>
中定义的FLT_RADIX
和FLT_MANT_DIG
静态计算此循环的结果。或者至少在理论上您可以做到,如果float
实际上适合IEEE浮点模型,而不是像Posit / unum这样的其他实数表示形式。
我不确定ISO C ++标准对float
的行为有何规定,以及不确定不是基于固定宽度指数和有效字段的格式是否符合标准。
在评论中:
@geza我想听听得到的电话号码!
@nada:是16777216
您是否声称要让此循环打印/返回16777216
?
更新:由于该评论已被删除,我认为没有。可能OP只是在无法完全表示为32位float
的第一个整数之前引用float
。 https://en.wikipedia.org/wiki/Single-precision_floating-point_format#Precision_limits_on_integer_values,即他们希望通过此错误代码验证的内容。
错误修复的版本当然会打印16777217
,它是不能可精确表示的第一个整数,而不是之前的值。
(所有较高的float值都是精确整数,但是对于大于有效宽度的指数值,它们是2,4,8等的倍数。可以表示许多较高的整数值,但是1个单位为(有效位的)最后一位大于1,因此它们不是连续的整数。最大的有限float
刚好在2 ^ 128以下,对于int64_t
来说太大了。)>
如果有任何编译器确实退出了原始循环并打印出来,那将是编译器错误。