这个浮点数优化是否被允许?

90

我试图找出 float 失去精确表示大整数的能力的位置。因此,我写了这个小片段:

int main() {
    for (int i=0; ; i++) {
        if ((float)i!=i) {
            return i;
        }
    }
}

这段代码似乎在所有编译器中都能工作,除了clang。Clang会生成一个简单的无限循环。Godbolt

这样做是否允许?如果是,这是否是一个质量问题(QoI)?


5
如果你使用-Ofast编译,gcc会执行相同的无限循环优化,所以它是一种被gcc认为不安全但仍然可以执行的优化。 - 12345ieee
请参考这个问题 - Calvin Godfrey
3
g++也会生成一个无限循环,但它不会优化掉其中的工作。你可以看到它执行了ucomiss xmm0,xmm0来将(float)i与自身进行比较。这是你第一个线索,表明你的C ++源代码并不是你想象的意思。你声称你让这个循环打印/返回16777216了吗?使用什么编译器/版本/选项?因为那将是一个编译器错误。gcc将你的代码正确地优化为jnp作为循环分支 (https://godbolt.org/z/XJYWeu):只要“!=”的操作数不是NaN就继续循环。 - Peter Cordes
4
具体而言,-Ofast选项隐式启用了-ffast-math选项,使得GCC能够应用不安全的浮点数优化,并因此生成与Clang相同的代码。 MSVC的行为完全相同:如果没有/fp: fast选项,它会生成一堆导致无限循环的代码;如果有/fp:fast选项,则会发出单个jmp指令。我假设,如果没有显式打开不安全的浮点数优化,这些编译器会被IEEE 754关于NaN值的要求卡住。相当有趣的是,事实上Clang并没有这个问题。其静态分析器更好。 - Cody Gray
1
@geza:如果代码实现了你的意图,即检查(float) i的数学值与i的数学值不同时,结果(在return语句中返回的值)将是16,777,217,而不是16,777,216。 - Eric Postpischil
显示剩余3条评论
2个回答

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

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

(float)i != (float)i

代码不应该失败,否则会导致代码最终溢出i,从而使你的程序产生未定义行为。因此,任何行为都是可能的。

为了正确检查您想要检查的内容,您应该将结果强制转换回int

if ((int)(float)i != i)

8
@Džuris这是未定义行为。并没有一个确定的结果。编译器可能会意识到它只能以未定义行为结束,并决定完全删除这个循环。 - anon
4
дҪ зҡ„ж„ҸжҖқжҳҜstatic_cast<int>(static_cast<float>(i))еҗ—пјҹеңЁйӮЈйҮҢдҪҝз”Ёreinterpret_castжҳҫ然жҳҜжңӘе®ҡд№үиЎҢдёәгҖӮ - Caleth
6
你是说(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 Cordes
2
@Caleth: reinterpret_cast<int>(float) 不完全是 UB,只是语法错误/不良形式。如果该语法允许将 float 强制转换为 int 进行类型切换作为 memcpy 的替代方案(后者是定义良好的),那就太好了,但我认为 reinterpret_cast<> 只适用于指针类型。 - Peter Cordes
2
@Deduplicator:是的,谢谢,我在回答时已经纠正了我之前评论中的错误。我在Godbolt上使用编译时常量NAN进行了测试。但愿我能编辑旧评论:P 我曾认为无序意味着任何谓词都是假的,但显然“!=”的工作方式类似于“!(x == x)”,而不是它自己的积极断言。 - Peter Cordes
显示剩余7条评论

50
正如@Angew指出的那样, !=运算符需要两侧类型相同。 (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。


顺便提一下,这意味着clang的优化也是安全的:它只需将(float)i != (implicit conversion to float)i视为相同,并证明int的可能范围内i->float永远不会是NaN即可。尽管这个循环会触发有符号溢出UB,但它允许发出任何想要的asm,包括一个ud2非法指令,或一个无限空循环,而不管循环体实际上是什么。但忽略有符号溢出UB,这种优化仍然是100%合法的。
即使使用-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不能为真。
这些编译器都优化了使用 IEEE 754 单精度(binary32)float 和 32 位 int 的 C++ 实现。修复错误的 (int)(float)i != i 循环在具有狭窄的 16 位 int 和/或更宽的 float 的 C++ 实现上会产生 UB,因为在到达第一个不能完全表示为 float 的整数之前,你将遇到有符号整数溢出 UB。但是,在针对像 gcc 或 clang 这样带有 x86-64 System V ABI 的实现进行编译时,不同一组实现定义的选择下的 UB 没有任何负面影响。
顺便说一句,你可以从<climits>中定义的FLT_RADIXFLT_MANT_DIG静态计算出此循环的结果。理论上,如果float实际上符合IEEE浮点数的模型而不是像Posit / unum这样的其他类型的实数表示,那么至少在理论上是可以的。
我不确定ISO C++标准对float行为有多少明确规定,以及是否基于固定宽度指数和尾数字段的格式将符合标准。

在评论中:

@geza我很想听到最终的数字!

@nada:是16777216

你是否声称已经通过此循环打印/返回16777216

更新:由于该评论已被删除,我认为不会。可能是OP只引用了第一个不能被32位float精确表示的整数之前的floathttps://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也无法表示。)

如果任何编译器退出原始循环并打印出来,那就是编译器的错误。


3
@SombreroChicken说:“不,我先学习了电子学(从我父亲留下的一些教科书开始学习;他是一名物理教授),然后是数字逻辑,之后才涉足CPU/软件方面。 :P 所以基本上我一直喜欢从底层理解事物,或者如果我从一个更高层次开始,我也喜欢至少学习一些影响我正在思考的层次下面的内容,以了解如何以及为什么工作。(例如,汇编语言的工作原理以及如何进行优化受到CPU设计约束/cpu架构等因素的影响。而这些因素又来自于物理学和数学。)” - Peter Cordes
1
GCC可能即使使用了frapw也无法进行优化,但我相信GCC 10的-ffinite-loops是为这种情况设计的。 - MCCCS

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