我是否有gcc优化错误或C代码问题?

10

请测试以下代码:

#include <stdio.h>
#include <stdlib.h>
main()
{
    const char *yytext="0";
    const float f=(float)atof(yytext);
    size_t t = *((size_t*)&f);
    printf("t should be 0 but is %d\n", t);
}

使用以下命令进行编译:

gcc -O3 test.c

良好的输出应该是:

"t should be 0 but is 0"

然而,我的gcc 4.1.3版本却出现了以下问题:

"t should be 0 but is -1209357172"

记住,问题总是出在你身上 - Tom Ritter
有趣的是,clang,llvm(llvm.org)的新C前端确实输出了正确的答案。 - Keltia
1
除了别名违规外,使用%d格式说明符打印size_t表达式是UB。您需要%zu,否则需要将其转换为int。此外,size_t可能比float大(例如,在64位机器上),因此您应该使用uint32_t,它始终与IEEE 754兼容实现中的float大小相同。 - R.. GitHub STOP HELPING ICE
9个回答

18

请使用编译器标志-fno-strict-aliasing。

启用严格别名检查时,至少在-O3默认情况下,在以下行中:

size_t t = *((size_t*)&f);

编译器假设size_t*不指向与float*相同的内存区域。据我所知,这是符合标准的行为(遵守ANSI标准中的严格别名规则始于gcc-4,正如Thomas Kammeyer所指出的那样)。
如果我没记错的话,您可以使用中间转换为char*来解决这个问题。(编译器认为char*可以别名任何东西)
换句话说,尝试这样做(我现在无法测试,但我认为它会起作用):
size_t t = *((size_t*)(char*)&f);

这并不值得新回答,但也许你可以编辑你的回答以表明在ANSI标准中遵守严格别名规则始于gcc-4。 - Thomas Kammeyer
是的,几乎任何3.4版本之后的东西都会抱怨它,但只有在您尝试取消引用它时才会出现。 - Tim Post

6
在C99标准中,以下规则在6.5-7中涵盖了这一点:
一个对象的存储值只能通过具有以下类型之一的lvalue表达式访问:73)
- 与对象的有效类型兼容的类型, - 与对象的有效类型兼容的类型的限定版本, - 与对象的有效类型相对应的带符号或无符号类型, - 与对象的有效类型的限定版本相对应的带符号或无符号类型, - 包含上述类型之一的聚合体或联合体类型(包括子聚合体或包含的联合体的成员,递归地),或 - 字符类型。
最后一项是为什么首先进行(char*)转换的原因。

5
根据C99关于指针别名的规则,这种写法不再被允许。两种不同类型的指针不能指向同一内存位置。但void和char指针是个例外。
所以在你的代码中,如果你将其转换为一个size_t类型的指针,编译器可以选择忽略它。如果你想要将float值作为size_t,只需将其指定并分配,float值将会被强制转换为size_t(截断而非四舍五入):
size_t size = (size_t)(f); // 这个方法可行
这通常被报告为一个bug,但实际上是一种功能,使优化器更有效地工作。
在gcc中,你可以使用编译器开关来禁用此功能。我相信是 -fno_strict_aliasing。

5

这是一段糟糕的 C 代码 :-)

问题在于你把一个 float 类型的对象强制转换成整数指针,并对其进行了解引用操作。

这违反了别名规则,编译器不能假设不同类型的指针(例如 float 或 int)在内存中不重叠。然而你却做到了。

编译器看到的是你计算了某个值,将其存储在 float 变量 f 中,但从未再次访问它。很可能编译器已经移除了部分代码,因此赋值操作从未发生过。

通过 size_t 指针进行解引用将返回堆栈中的一些未初始化垃圾数据。

你可以做两件事来解决这个问题:

  1. 使用带有 float 和 size_t 成员的 union,并通过类型转换来完成操作。虽然不太优美,但可行。

  2. 使用 memcopy 将 f 的内容复制到 size_t 中,编译器足够智能,可以检测并优化此场景。


抱歉有点挑剔,但在解决方法1中应该是类型转换(type punning)而不是修剪(prunning)。 - James Caccese

3

你为什么认为 t 应该是 0 呢?

更准确地说,为什么你认为浮点数 0 的二进制表示和整数 0 的二进制表示相同呢?


我认为这是不可能的。这意味着一个可执行文件的所有库都需要使用相同的优化级别进行编译(否则,调用一个接受浮点数的函数将会失败)。 - bortzmeyer

1

这是糟糕的 C 代码。你的强制类型转换违反了 C 的别名规则,优化器可以自由地执行可能会破坏此代码的操作。你可能会发现 GCC 已经将 size_t 读取安排在浮点数写入之前(以隐藏 fp 管道延迟)。

你可以设置 -fno-strict-aliasing 开关,或使用 union 或 reinterpret_cast 以符合标准的方式重新解释值。


1
除了指针对齐之外,您期望sizeof(size_t)==sizeof(float)。我认为它不是(在64位Linux上,size_t应该是64位,但float是32位),这意味着您的代码将读取未初始化的内容。

-1

-O3并不被认为是“明智的选择”,-O2通常是上限,除了一些多媒体应用程序可能需要更高的优化级别。

有些应用程序甚至无法达到-O1的优化级别,如果你超过了这个级别,它们就会崩溃。

如果你使用的是足够新的GCC(我这里使用的是4.3版本),它可能支持这个命令。

  gcc -c -Q -O3 --help=optimizers > /tmp/O3-opts

如果你细心一点,可能能够浏览这个列表并找到导致这个bug的独立优化。

来自man gcc

  The output is sensitive to the effects of previous command line options, so for example it is possible to find out which
       optimizations are enabled at -O2 by using:

               -O2 --help=optimizers

       Alternatively you can discover which binary optimizations are enabled by -O3 by using:

               gcc -c -Q -O3 --help=optimizers > /tmp/O3-opts
               gcc -c -Q -O2 --help=optimizers > /tmp/O2-opts
               diff /tmp/O2-opts /tmp/O3-opts | grep enabled

-2
我用如下编译器测试了你的代码: "i686-apple-darwin9-gcc-4.0.1 (GCC) 4.0.1 (Apple Inc. build 5465)" 没有问题。 输出:
t should be 0 but is 0

所以你的代码没有错误,并不意味着它是好的代码。 但是我会添加主函数的返回类型和在函数末尾添加"return 0;"。


1
代码在单一平台上产生预期结果并不意味着它是正确的证明! - Joachim Sauer
这个bug在OS X上没有发生,我遇到了Linux系统上的问题,但Windows和OS X是正常工作的。 - akauppi

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