我试图检查哪里float
失去了精确表示大整数的能力。所以我写了这个小片段:
int main() {
for (int i=0; ; i++) {
if ((float)i!=i) {
return i;
}
}
}
该代码似乎适用于所有编译器,但不包括clang。Clang生成一个简单的无限循环。上帝保佑。
可以吗?如果是,那是QoI问题吗?
我试图检查哪里float
失去了精确表示大整数的能力。所以我写了这个小片段:
int main() {
for (int i=0; ; i++) {
if ((float)i!=i) {
return i;
}
}
}
该代码似乎适用于所有编译器,但不包括clang。Clang生成一个简单的无限循环。上帝保佑。
可以吗?如果是,那是QoI问题吗?
gcc
如果您进行编译,则会执行相同的无限循环优化-Ofast
,因此,它gcc
认为优化是不安全的,但它可以做到。
ucomiss xmm0,xmm0
可以(float)i
与自身进行比较。这是您的第一个线索,即您的C ++源代码并不意味着您认为的那样。您是否声称要让此循环打印/返回16777216
?那是什么编译器/版本/选项?因为那将是编译器错误。gcc正确地将您的代码优化jnp
为循环分支(godbolt.org/z/XJYWeu):只要操作数!=
不是NaN ,就继续循环。
-ffast-math
是隐式启用的选项,-Ofast
它允许GCC应用不安全的浮点优化,从而生成与Clang相同的代码。MSVC的行为完全相同:没有/fp:fast
,它会生成一堆导致无限循环的代码;使用/fp:fast
,它发出一条jmp
指令。我假设没有明确打开不安全的FP优化,这些编译器就无法满足有关NaN值的IEEE 754要求。实际上,Clang并不有趣。它的静态分析仪更好。@ 12345ieee
(float) i
不同于的数学值i
,则结果(return
语句中返回的值)将为16,777,217,而不是16,777,216。
Answers:
正如@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,但实际上它允许发出任何想要的asm,包括ud2
非法指令或空的无限循环,而不管循环主体是什么。)但是忽略有符号溢出的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_RADIX
和静态计算此循环的结果。或者至少在理论上,如果实际上适合IEEE浮点模型,而不是像Posit / unum这样的其他实数表示形式,则至少可以。FLT_MANT_DIG
<climits>
float
我不确定ISO C ++标准对float
行为有多大的规定,以及不确定不是基于固定宽度指数和有效字段的格式是否符合标准。
在评论中:
@geza我想听听得到的电话号码!
@nada:是16777216
您是否声称要让此循环打印/返回16777216
?
更新:由于该评论已被删除,我认为没有。可能OP只是float
在不能完全表示为32位的第一个整数之前引用float
。 https://zh.wikipedia.org/wiki/Single-precision_floating-point_format#Precision_limits_on_integer_values, 即他们希望通过此错误代码进行验证的内容。
错误修复的版本当然会打印出16777217
,第一个不能精确表示的整数,而不是之前的值。
(所有较高的浮点值都是精确的整数,但是对于大于有效宽度的指数值,它们是2,4,8的倍数。可以表示许多较高的整数,但是最后一个单位(有效数的)大于1,因此它们不是连续的整数。最大的有限float
值刚好在2 ^ 128以下,对于偶数而言太大int64_t
。)
如果有任何编译器确实退出了原始循环并打印出来,那将是编译器错误。
frapw
,GCC可能也无法优化,但是我敢肯定,GCC 10 -ffinite-loops
是针对此类情况设计的。
请注意,内置运算符!=
要求其操作数具有相同的类型,并将在必要时使用提升和转换来实现该操作数。换句话说,您的情况等同于:
(float)i != (float)i
那将永远不会失败,因此代码最终将溢出i
,从而使您的程序具有未定义的行为。因此,任何行为都是可能的。
要正确检查您要检查的内容,您应该将结果投射回int
:
if ((int)(float)i != i)
static_cast<int>(static_cast<float>(i))
?reinterpret_cast
在那里很明显是UB
(int)(float)i != i
是说UB吗?您如何得出结论?是的,它取决于实现定义的属性(因为float
不需要是IEEE754 binary32),但是在任何给定的实现上,它都是定义明确的,除非float
可以精确表示所有正值,否则int
我们将得到带符号整数的溢出UB。(en.cppreference.com/w/cpp/types/climits定义FLT_RADIX
并FLT_MANT_DIG
确定)。在一般的印刷实现定义的事物中,例如std::cout << sizeof(int)
不是UB ...
reinterpret_cast<int>(float)
并不完全是UB,它只是语法错误/格式错误。我认为,如果该语法允许将float的类型进行int
对等处理memcpy
(定义明确),但reinterpret_cast<>
仅适用于指针类型,那会很好。
x != x
是真的。现场观看coliru。在C中也是如此。