是否允许这种浮点优化?


90

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

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

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

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


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

5
gcc如果您进行编译,则会执行相同的无限循环优化-Ofast,因此,它gcc认为优化是不安全的,但它可以做到。
12345ieee19年

3
g ++还会生成一个无限循环,但它并不能从内部优化工作。您可以看到它确实ucomiss xmm0,xmm0可以(float)i与自身进行比较。这是您的第一个线索,即您的C ++源代码并不意味着您认为的那样。您是否声称要让此循环打印/返回16777216?那是什么编译器/版本/选项?因为那将是编译器错误。gcc正确地将您的代码优化jnp为循环分支(godbolt.org/z/XJYWeu):只要操作数!= 不是NaN ,就继续循环。
彼得·科德斯

4
具体来说,它-ffast-math是隐式启用的选项,-Ofast它允许GCC应用不安全的浮点优化,从而生成与Clang相同的代码。MSVC的行为完全相同:没有/fp:fast,它会生成一堆导致无限循环的代码;使用/fp:fast,它发出一条jmp指令。我假设没有明确打开不安全的FP优化,这些编译器就无法满足有关NaN值的IEEE 754要求。实际上,Clang并不有趣。它的静态分析仪更好。@ 12345ieee
Cody Gray

1
@geza:如果代码符合您的预期,请检查的数学值何时(float) i不同于的数学值i,则结果(return语句中返回的值)将为16,777,217,而不是16,777,216。
埃里克·波斯特皮希尔

Answers:


49

正如@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位的第一个整数之前引用floathttps://zh.wikipedia.org/wiki/Single-precision_floating-point_format#Precision_limits_on_integer_values, 即他们希望通过此错误代码进行验证的内容。

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

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

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


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

1
即使使用frapw,GCC可能也无法优化,但是我敢肯定,GCC 10 -ffinite-loops是针对此类情况设计的。
MCCCS

64

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

(float)i != (float)i

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

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

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

8
@Džuris是UB。目前没有一个明确的结果。编译器可能意识到,它只能以UB结尾,并决定完全删除循环。
基金莫妮卡的诉讼

4
@opa是什么意思static_cast<int>(static_cast<float>(i))reinterpret_cast在那里很明显是UB
Caleth

6
@NicHartley:您(int)(float)i != i是说UB吗?您如何得出结论?是的,它取决于实现定义的属性(因为float不需要是IEEE754 binary32),但是在任何给定的实现上,它都是定义明确的,除非float可以精确表示所有正值,否则int我们将得到带符号整数的溢出UB。(en.cppreference.com/w/cpp/types/climits定义FLT_RADIXFLT_MANT_DIG确定)。在一般的印刷实现定义的事物中,例如std::cout << sizeof(int)不是UB ...
Peter Cordes

2
@Caleth:reinterpret_cast<int>(float)并不完全是UB,它只是语法错误/格式错误。我认为,如果该语法允许将float的类型进行int对等处理memcpy(定义明确),但reinterpret_cast<>仅适用于指针类型,那会很好。
彼得·科德斯

2
@Peter就NaN而言,x != x是真的。现场观看coliru。在C中也是如此。
Deduplicator
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.