为什么GCC和Clang不进行混叠优化?

时间:2013-06-19 14:14:51

标签: c++ gcc optimization compiler-optimization strict-aliasing

I have a case朋友将类型为“Base”的非基类对象强制转换为类型对象“Derived”,其中“Derived”是“Base”的派生类,只添加函数,但是没有数据。在下面的代码中,我 将数据成员x添加到派生类

struct A {
  int a;
};

struct B : A {
  // int x;
  int x;
};

A a;

int g(B *b) {
   a.a = 10;
   b->a++;
   return a.a;
}

通过严格的别名分析,GCC(也是Clang)总是返回10而不是11,因为b永远不会指向明确定义的a码。但是,如果我删除B::x(就像我朋友的代码中的情况一样),GCC的输出汇编代码优化a.a的返回访问权限并重新加载来自记忆的价值。所以我的朋友的代码调用g“在GCC上工作”(正如他的意图),即使我认为它仍然有未定义的行为

g((B*)&a);

所以在基本相同的两种情况下,GCC优化了一种情况并且没有优化其他情况。是因为b可以合法地点到a?或者是因为GCC只是想破坏现实世界的代码?


我测试了陈述

的答案
  

如果删除B :: x,那么B符合9p7中对标准布局类的要求,并且访问变得非常明确,因为这两种类型是布局兼容的,9.2p17。

使用两个布局兼容的枚举

enum A : int { X, Y };
enum B : int { Z };

A a;

int g(B *b) {
   a = Y;
   *b = Z;
   return a;
}

g的汇编程序输出返回1,而不是0,即使AB布局兼容(7.2p8)。


所以我的进一步问题是(引用答案):“两个具有完全相同布局的类可能被认为”几乎相同“并且它们被排除在优化之外。” 有人可以为GCC或Clang提供此证明吗?

4 个答案:

答案 0 :(得分:8)

如果删除B::x,则B符合9p7中标准布局类的要求,并且访问权限变得非常明确,因为这两种类型< em> layout-compatible ,9.2p17和成员都有相同的类型。


  

标准布局类是一个类:

     
      
  • 没有非标准布局类(或此类类型的数组)或引用类型的非静态数据成员,
  •   
  • 没有虚函数(10.3),没有虚基类(10.1),
  •   
  • 对所有非静态数据成员具有相同的访问控制(第11条),
  •   
  • 没有非标准布局基类
  •   
  • 在大多数派生类中没有非静态数据成员,并且最多只有一个具有非静态数据成员的基类,或者没有包含非静态数据成员的基类,并且< / LI>   
  • 没有与第一个非静态数据成员相同类型的基类。
  •   

  

如果两个标准布局结构类型具有相同数量的非静态数据成员,并且相应的非静态数据成员(按声明顺序)具有布局兼容类型,则它们是布局兼容的。

答案 1 :(得分:6)

未定义的行为包括 工作的情况,即使它不应该。

根据此联合的标准用法,允许访问标头或数据成员的类型和大小字段:

union Packet {
   struct Header {
   short type;
   short size;  
   } header;
   struct Data {
   short type;
   short size;  
   unsigned char data[MAX_DATA_SIZE];
   } data;
}

这严格限于工会,但许多编译器支持这种扩展,只要“不完整”类型将以未定义大小的数组结束。如果从子类中删除额外的静态非成员,它确实变得微不足道并且布局兼容,这允许别名?

struct A {
  int a;
};

struct B  {
  int a;
  //int x;
};

A a;

int g(B *b) {
   a.a = 10;
   b->a++;
   return a.a;
}

仍然执行别名优化。在具有相同数量的非静态成员的情况下,假定最派生的类与基类相同。让我们颠倒顺序:

#include <vector>
#include <iostream>

struct A {
  int a;
};

struct B : A  {
  int x;
};

B a;

int g(A *b) {
   a.a = 10;
   b->a++;
   return a.a;
}

int main()
{
    std::cout << g((A*)&a);
}

这会按预期返回11,因为B显然也是A,与原始尝试不同。让我们进一步发挥

struct A {
  int a;
};

struct B : A {
    int  foo() { return a;}
};

除非foo()是虚拟的,否则不会导致别名优化。将非静态或const成员添加到B将导致“10”答案,添加非平凡构造函数或静态不会。

PS。 在第二个例子中

enum A : int { X, Y };
enum B : int { Z };

这两者之间的布局兼容性由C ++ 14定义,并且它们与底层类型(但可转换)不兼容。虽然像

之类的东西
 enum A a = Y;
 enum B b = (B*)a;

可能会产生未定义的行为,就像您尝试使用任意32位值映射float一样。

答案 2 :(得分:0)

我认为您的代码是UB,因为您要取消引用来自违反type aliasing rules的转换的指针。

现在,如果激活严格别名标志,则允许编译器优化UB的代码。如何使用此UB取决于编译器。您可以看到this question的答案。

关于gcc,documentation for -fstrict-aliasing显示它可以根据以下内容进行优化:

  

(...)假设一种类型的对象永远不会驻留在同一个对象上   地址作为不同类型的对象,除非类型几乎   同样的。

我无法找到&#34;几乎相同&#34;的定义,但可以考虑具有完全相同布局的两个类&#34;几乎相同&#34;并且它们被排除在优化之外。

答案 3 :(得分:0)

我相信以下是合法的C ++(不调用UB):

#include <new>

struct A {
  int a;
};

struct B : A {
  // int x;
};

static A a;

int g(B *b);
int g(B *b) {
   a.a = 10;
   b->a++;
   return a.a;
}

int f();
int f() {
  auto p = new (&a) B{};
  return g(p);
}

因为(全局)a总是引用A类型的对象(即使它是B - 对象的子对象,在调用{{1}之后}}和f()指向p类型的对象。

如果您将B标记为a存储持续时间(就像我上面所做的那样),我测试的所有编译器都会很乐意应用严格的别名并优化以返回static

另一方面,如果您使用10标记g()或添加返回指向__attribute__((noinline))

的指针的函数h()
a

我测试的编译器假设A* h(); A* h() { return &a; } 和参数&a可以别名并重新加载值。