允许这种浮点优化吗?
Posted
技术标签:
【中文标题】允许这种浮点优化吗?【英文标题】:Is this floating-point optimization allowed? 【发布时间】:2019-11-22 00:57:32 【问题描述】:我试图检查float
在哪里失去了精确表示大整数的能力。所以我写了这个小sn-p:
int main()
for (int i=0; ; i++)
if ((float)i!=i)
return i;
此代码似乎适用于所有编译器,除了 clang。 Clang 生成一个简单的无限循环。 Godbolt.
这是允许的吗?如果是,是 QoI 问题吗?
【问题讨论】:
@geza 我很想知道结果数字!gcc
如果您使用-Ofast
编译,则会执行相同的无限循环优化,因此gcc
认为这是不安全的优化,但它可以做到。
g++ 也会生成一个无限循环,但它不会优化其中的工作。你可以看到 ucomiss xmm0,xmm0
将 (float)i
与自身进行比较。这是你的第一个线索,你的 C++ 源代码并不意味着你认为它做了什么。你是否声称你有这个循环来打印/返回16777216
?那是什么编译器/版本/选项?因为那将是一个编译器错误。 gcc 正确地将您的代码优化为 jnp
作为循环分支 (godbolt.org/z/XJYWeu):只要 !=
的操作数不是 NaN,就一直循环。
具体来说,-Ofast
隐式启用的 -ffast-math
选项允许 GCC 应用不安全的浮点优化,从而生成与 Clang 相同的代码。 MSVC 的行为方式完全相同:没有/fp:fast
,它会生成一堆导致无限循环的代码;使用/fp:fast
,它会发出一条jmp
指令。我假设在没有明确打开不安全的 FP 优化的情况下,这些编译器会挂断关于 NaN 值的 IEEE 754 要求。实际上,Clang 没有,这很有趣。它的静态分析器更好。 @12345ieee
@geza: 如果代码符合您的预期,检查(float) i
的数学值何时与i
的数学值不同,然后是结果(@987654340 中返回的值@ 语句)将是 16,777,217,而不是 16,777,216。
【参考方案1】:
请注意,内置运算符!=
要求其操作数的类型相同,必要时将使用提升和转换来实现。换句话说,您的条件相当于:
(float)i != (float)i
这永远不会失败,因此代码最终会溢出i
,给你的程序带来未定义的行为。因此任何行为都是可能的。
要正确检查您要检查的内容,您应该将结果转换回int
:
if ((int)(float)i != i)
【讨论】:
@Džuris 这是 UB。没有一个确定的结果。编译器可能会意识到它只能以 UB 结尾并决定完全删除循环。 @opa 你的意思是static_cast<int>(static_cast<float>(i))
? reinterpret_cast
那里明显是 UB
@NicHartley:你是说(int)(float)i != i
是UB?你如何得出结论?是的,它取决于 实现定义 属性(因为 float
不需要是 IEEE754 二进制 32),但在任何给定的实现上,它都是明确定义的,除非 float
可以准确地表示所有积极的 int
值,所以我们得到有符号整数溢出 UB。 (en.cppreference.com/w/cpp/types/climits 定义 FLT_RADIX
和 FLT_MANT_DIG
确定这一点)。一般来说打印实现定义的东西,比如std::cout << sizeof(int)
不是UB...
@Caleth: reinterpret_cast<int>(float)
不完全是 UB,它只是一个语法错误/格式错误。如果该语法允许将 float 类型双关以 int
作为 memcpy
的替代品(定义明确),那就太好了,但我认为 reinterpret_cast<>
仅适用于指针类型。
@Peter Just for NaN,x != x
是真的。 See live on coliru。在 C 中也是如此。【参考方案2】:
As @Angew pointed out,!=
运算符需要两边的类型相同。
(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
为假,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
在 @987654345 的可能范围内绝不是 NaN @。
(尽管这个循环会遇到有符号溢出 UB,但它实际上可以发出任何它想要的 asm,包括 ud2
非法指令,或者一个空的无限循环,不管循环体实际上是什么。)但是忽略签名溢出 UB,这种优化仍然是 100% 合法的。
GCC 未能优化掉循环体即使使用-fwrapv
来明确定义有符号整数溢出(作为 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++ 实现进行了优化。
bugfixed (int)(float)i != i
循环将在具有窄 16 位 int
和/或更宽 float
的 C++ 实现上具有 UB,因为在到达之前您会遇到有符号整数溢出 UB第一个不能完全表示为 float
的整数。
但是,在使用 x86-64 System V ABI 为 gcc 或 clang 等实现进行编译时,在一组不同的实现定义选择下的 UB 不会产生任何负面影响。
顺便说一句,您可以根据<climits>
中定义的FLT_RADIX
和FLT_MANT_DIG
静态计算此循环的结果。或者至少理论上你可以,如果 float
实际上适合 IEEE 浮点模型,而不是像 Posit / unum 之类的其他实数表示。
我不确定 ISO C++ 标准对float
行为的规定有多少,以及不基于固定宽度指数和有效数字字段的格式是否符合标准。
在 cmets 中:
@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
来说也太大了。)
如果任何编译器确实退出了原始循环并打印出来,那将是编译器错误。
【讨论】:
@SombreroChicken:不,我首先学习了电子学(从我父亲身边的一些教科书;他是一名物理学教授),然后是数字逻辑,然后进入 CPU/软件。 :P 所以我一直很喜欢从头开始理解事物,或者如果我从更高级别开始,那么我至少喜欢了解以下级别的知识,这会影响事物在我所在级别的工作方式/原因思考。 (例如,asm 如何工作以及如何优化它受 CPU 设计约束/cpu 架构的影响。这又来自物理 + 数学。) 即使使用frapw
,GCC 也可能无法优化,但我确信 GCC 10 的 -ffinite-loops
是为此类情况设计的。以上是关于允许这种浮点优化吗?的主要内容,如果未能解决你的问题,请参考以下文章