我试图找出 float
失去精确表示大整数的能力的位置。因此,我写了这个小片段:
int main() {
for (int i=0; ; i++) {
if ((float)i!=i) {
return i;
}
}
}
这段代码似乎在所有编译器中都能工作,除了clang。Clang会生成一个简单的无限循环。Godbolt。
这样做是否允许?如果是,这是否是一个质量问题(QoI)?
我试图找出 float
失去精确表示大整数的能力的位置。因此,我写了这个小片段:
int main() {
for (int i=0; ; i++) {
if ((float)i!=i) {
return i;
}
}
}
这段代码似乎在所有编译器中都能工作,除了clang。Clang会生成一个简单的无限循环。Godbolt。
这样做是否允许?如果是,这是否是一个质量问题(QoI)?
!=
要求其操作数为相同类型,并在必要时使用提升和转换来实现。换句话说,您的条件等价于:
请注意,内置运算符!=
要求其操作数为相同类型,并在必要时使用提升和转换来实现。换句话说,您的条件等价于:
(float)i != (float)i
代码不应该失败,否则会导致代码最终溢出i
,从而使你的程序产生未定义行为。因此,任何行为都是可能的。
为了正确检查您想要检查的内容,您应该将结果强制转换回int
:
if ((int)(float)i != i)
static_cast<int>(static_cast<float>(i))
еҗ—пјҹеңЁйӮЈйҮҢдҪҝз”Ёreinterpret_cast
жҳҫ然жҳҜжңӘе®ҡд№үиЎҢдёәгҖӮ - Caleth(int)(float)i != i
是未定义行为吗?你是如何得出这个结论的?是的,它取决于实现定义的属性(因为float
不需要是IEEE754 binary32),但在任何给定的实现中,除非float
可以精确表示所有正的int
值,否则它是定义良好的,因此我们会得到符号整数溢出UB。(https://en.cppreference.com/w/cpp/types/climits定义了`FLT_RADIX`和`FLT_MANT_DIG`来确定这一点)。通常情况下,打印实现定义的内容,比如`std::cout << sizeof(int)`是不是UB... - Peter Cordesreinterpret_cast<int>(float)
不完全是 UB,只是语法错误/不良形式。如果该语法允许将 float 强制转换为 int 进行类型切换作为 memcpy 的替代方案(后者是定义良好的),那就太好了,但我认为 reinterpret_cast<>
只适用于指针类型。 - Peter Cordes!=
运算符需要两侧类型相同。 (float)i != i
会将右侧提升为float,因此我们得到(float)i != (float)i
。
g++也会生成一个无限循环,但它不会优化其中的工作。你可以看到它使用cvtsi2ss
将int->float进行转换,并使用ucomiss xmm0,xmm0
将(float)i
与自身进行比较。(这是你的第一个线索,说明@Angew回答的那样,你的C++源代码并不是你想象的那样。)
x != x
仅在"unordered"时为真,因为x
是NaN。(在IEEE math中,INFINITY
与自身相等,但NaN不相等。NAN == NAN
为false,NAN != NAN
为true)。
gcc7.4及更早版本正确地将您的代码优化为jnp
作为循环分支(https://godbolt.org/z/fyOhW1):只要x != x
的操作数不是NaN,就继续循环。(gcc8及更高版本还检查je
以跳出循环,未能根据任何非NaN输入始终为true的事实进行优化)。x86 FP比较将设置PF为unordered。
-fwrapv
使有符号整数溢出被定义为2的补码环绕,GCC也无法优化掉循环体https://godbolt.org/z/t9A8t_。即使启用-fno-trapping-math
也没有帮助(GCC的默认设置不幸的是启用-ftrapping-math
,尽管其实现存在错误/bug)。int->float转换可能会引发FP不精确异常(对于无法准确表示的过大数字),因此在异常可能未被屏蔽的情况下,不优化掉循环体是合理的。(因为将16777217
转换为float可能会产生可观察的副作用,如果不精确异常未被屏蔽的话。)但是使用-O3 -fwrapv -fno-trapping-math
后,将此编译为空的无限循环是100%的被忽略的优化。没有#pragma STDC FENV_ACCESS ON
,记录掩盖的FP异常的粘性标志的状态不是代码的可观察副作用。没有任何int->float转换会导致NaN,因此x != x
不能为真。
<climits>
中定义的FLT_RADIX
和FLT_MANT_DIG
静态计算出此循环的结果。理论上,如果float
实际上符合IEEE浮点数的模型而不是像Posit / unum这样的其他类型的实数表示,那么至少在理论上是可以的。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
,而不是它之前的值,这是第一个不能完全表示的整数。
(所有更高的浮点值都是精确的整数,但它们是2、4、8等指数值的倍数。许多更高的整数值可以表示,但最后一位(有效数字)的1单位大于1,因此它们不是连续的整数。最大的有限float
略小于2^128,这太大了,即使是int64_t
也无法表示。)
如果任何编译器退出原始循环并打印出来,那就是编译器的错误。
-Ofast
编译,gcc
会执行相同的无限循环优化,所以它是一种被gcc
认为不安全但仍然可以执行的优化。 - 12345ieeeucomiss xmm0,xmm0
来将(float)i
与自身进行比较。这是你第一个线索,表明你的C ++源代码并不是你想象的意思。你声称你让这个循环打印/返回16777216
了吗?使用什么编译器/版本/选项?因为那将是一个编译器错误。gcc将你的代码正确地优化为jnp
作为循环分支 (https://godbolt.org/z/XJYWeu):只要“!=”的操作数不是NaN就继续循环。 - Peter Cordes-Ofast
选项隐式启用了-ffast-math
选项,使得GCC能够应用不安全的浮点数优化,并因此生成与Clang相同的代码。 MSVC的行为完全相同:如果没有/fp: fast
选项,它会生成一堆导致无限循环的代码;如果有/fp:fast
选项,则会发出单个jmp
指令。我假设,如果没有显式打开不安全的浮点数优化,这些编译器会被IEEE 754关于NaN值的要求卡住。相当有趣的是,事实上Clang并没有这个问题。其静态分析器更好。 - Cody Gray(float) i
的数学值与i
的数学值不同时,结果(在return
语句中返回的值)将是16,777,217,而不是16,777,216。 - Eric Postpischil