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

时间:2020-03-05 18:59:02  来源:igfitidea点击:

测试以下代码:

#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

GOOD输出应为:

"t should be 0 but is 0"

但是在我的gcc 4.1.3中,我有:

"t should be 0 but is -1209357172"

解决方案

回答

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

启用严格别名,因为默认情况下至少在行中为-O3:

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

编译器假定size_t *不指向与float *相同的内存区域。据我所知,这是符合标准的行为(如Thomas Kammeyer所指出的,遵循ANSI标准中严格的别名规则始于gcc-4)。

如果我没记错的话,可以使用一个中间的强制转换为char *来解决这个问题。 (编译器假定char *可以为任何别名)

换句话说,尝试一下(目前无法自己测试,但我认为它可以工作):

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

回答

我们为什么认为t应该为0?

或者更准确地说,"为什么我们认为浮点零的二进制表示形式与整数零的二进制表示形式相同?"

回答

-O3不被视为"理智",-O2通常是上限,但某些多媒体应用程序除外。

某些应用程序甚至无法走得那么远,如果超出-O1就会死掉。

如果我们有足够新的GCC(我在此处为4.3),它可能支持此命令

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

如果我们小心的话,我们可能可以浏览该列表,找到正在启用的给定单数优化,这将导致此错误。

来自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

回答

根据指针别名的C99规则,不再允许这样做。两种不同类型的指针不能指向内存中的相同位置。该规则的例外是void和char指针。

因此,在要转换为size_t指针的代码中,编译器可以选择忽略它。如果我们想将float值作为size_t来获取,只需对其进行分配,就可以将float转换为(舍入后不四舍五入)如下:

size_t size =(size_t)(f); //这有效

通常将其报告为错误,但实际上实际上是一项允许优化器更有效地工作的功能。

在gcc中,我们可以使用编译器开关禁用此功能。我相信-fno_strict_aliasing。

回答

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

我们可以设置-fno-strict-aliasing开关,或者使用并集或者reinterpret_cast以符合标准的方式重新解释该值。

回答

这是糟糕的C代码:-)

问题部分在于,我们可以通过将float类型的一个对象强制转换为整数指针并取消引用来访问它。

这违反了别名规则。编译器可以自由地假定指向不同类型(例如float或者int)的指针在内存中不会重叠。我们已经做到了。

编译器看到的是我们计算了一些东西,将其存储在float f中,再也无法访问它了。编译器很可能已删除了部分代码,并且分配从未发生过。

在这种情况下,通过size_t指针进行的取消引用将从堆栈中返回一些未初始化的垃圾。

我们可以做两件事来解决此问题:

  • 使用带有float和size_t成员的并集,并通过punning类型进行转换。不好,但是可以。
  • 使用内存复制将f的内容复制到size_t中。编译器足够聪明,可以检测和优化这种情况。

回答

我用以下方法测试了代码:
" i686-apple-darwin9-gcc-4.0.1(GCC)4.0.1(Apple Inc.内部版本5465)"

并没有问题。
输出:

t should be 0 but is 0

因此,代码中没有错误。这并不意味着它是好的代码。
但是我要添加main函数的returntype和" return 0;"。在函数的末尾。

回答

在C99标准中,这由6.5-7中的以下规则涵盖:

An object shall have its stored value accessed only by an lvalue expression that has one of
  the following types:73)
  
  
  a type compatible with the effective type of the object,
  a qualified version of a type compatible with the effective type of the object,
  a type that is the signed or unsigned type corresponding to the effective type of the
  object,
  a type that is the signed or unsigned type corresponding to a qualified version of the
  effective type of the object,
  an aggregate or union type that includes one of the aforementioned types among its
  members (including, recursively, a member of a subaggregate or contained union), or
  a character type.

最后一项是为什么首先强制转换为(char *)起作用的原因。

回答

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