【问题标题】:Why doesn't GCC and Clang do this aliasing-optimization?为什么 GCC 和 Clang 不做这种别名优化?
【发布时间】:2013-06-16 03:03:54
【问题描述】:

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 的返回访问并从内存中重新加载值。所以我朋友的代码在 GCC 上调用 g“工作”(如他所愿),即使我认为它仍然具有未定义的行为

g((B*)&a);

所以在本质上相同的两种情况下,GCC 优化了一种情况而没有优化另一种情况。 是不是因为b 可以合法地 指向@ 987654333@?还是因为 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 提供证明吗?

【问题讨论】:

  • @MatsPetersson g 被称为g((B*)&a)。在我的测试 sn-p 中,它没有被调用(我只需要 g 的汇编器输出)
  • 您的问题应直接发送至 GCC 邮件列表。为什么某些实现会在未定义的行为情况下这样做?因为实现的细节通过一些内部表示加起来。此外,如果这是一个 GCC 主义,它可能会通过使用 -std=c++11 而不是 -std=gnu++11 而消失。
  • 当出现关于未定义行为的问题时,我不喜欢某些人的心态。未定义的行为并不意味着编译器开发人员关闭他们的大脑并玩骰子。这也不意味着他们不在乎。
  • 我同意,我发现了 3.10p10 - 不幸的是,我的图形驱动程序决定停止工作。我同意,未定义只是意味着由编译器供应商做一些尽可能有意义的事情——但有些情况很难做到,编译器要“意识到” aa 和 b->a 是同样在这里[或没有意识到,但要安全行事],他们将不得不不必要地存储和加载数据,以防万一您的 b 对象不应该是指向 a 对象的指针实际上是同样a.a。人们更喜欢编写正确的快速代码。
  • 我在 GCC、clang、ICC 上对其进行了测试,都产生了相同的程序集,如 JohannesSchaub-litb 所述。我还尝试了这些编译器的不同版本。他们3个都同意。如果B 没有声明任何非静态数据成员或任何虚函数,则编译器假定*b 可以是a 的别名。也许早期的优化会替换不添加任何数据(包括 vptr)的派生类?

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


【解决方案1】:

我相信以下是合法的 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 类型的对象(即使在调用 f() 之后它是 B 对象的子对象)并且 p 指向输入B

如果您将a 标记为具有static 存储持续时间(正如我在上面所做的那样),我测试过的所有编译器都会很高兴地应用严格别名并优化以返回10

另一方面,如果您将g() 标记为__attribute__((noinline)) 或添加一个函数h(),该函数返回一个指向a 的指针

A* h();
A* h() { return &a; }

我测试过的编译器假定&amp;a 和参数b 可以别名并重新加载值。

【讨论】:

    【解决方案2】:

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

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

    关于 gcc,documentation for -fstrict-aliasing 表明它可以基于:

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

    我还没有找到“几乎相同”的定义,但是具有完全相同布局的两个类可能被认为“几乎相同”并且它们被排除在优化之外。

    【讨论】:

    • 单独的函数“g”没有未定义的行为,因此这无法解释两个编译器优化器输出的差异。
    • 当你激活 -fstrict-alisiang 标志时,你告诉编译器它可以优化假设你没有做任何由于别名导致 UB 的事情。您只能通过 UB 到达 b == &a。因此,优化器注意到 b != &a 和 g() 将始终返回 10。所以我认为真正的问题是为什么优化器并不总是使用这种优化,具体取决于 B 声明。我在标准上找不到任何东西,但我发现文档谈到了“几乎相同类型”的非标准概念。
    【解决方案3】:

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

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

    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() 是虚拟的。向 B 添加非静态或 const 成员将导致“10”答案,添加非平凡构造函数或静态不会。

    附言。 在第二个例子中

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

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

     enum A a = Y;
     enum B b = (B*)a;
    

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

    【讨论】:

    • 这让我很困惑:“不过,仍然执行别名优化。”。因此,当 A 和 B 是布局兼容的不相关类(实际上,在结构上 等效!)时,它会进行优化。但是,如果一切都是平等的,但只有像我的测试用例中那样的基派生关系,那么它就不会进行优化。这表明,这不仅仅是“布局兼容性”或结构等价的事情,而是与继承有关,对吧?还是我误解了答案?
    • 奖励这个,因为它是迄今为止最有帮助的答案。
    • @Johannes Schaub - litb 具有讽刺意味的是,我的学生在遇到此代码挑战时,指着 (B*)&A 并说“那是错误的”,他们的理解完全基于基数和派生数的关系类(并且他们知道底层地址机制) ?如果它们不相关,则“A 不是 B”,“B 也是 A”但不“相等”,当 A 是 B 的基类时,“指向 A 的指针可能是指向 B 的指针”,
    【解决方案4】:

    如果您删除 B::x,那么 B 满足 9p7 中对标准布局类的要求,并且访问变得完美定义,因为这两种类型是 layout-兼容,9.2p17 和成员都具有相同的类型。


    标准布局类是这样的类:

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

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

    【讨论】:

    • @BenVoigt 该项目符号不适用于此类情况。否则,您可以使用任意结构来访问 int,前提是该结构也包含该 int。如果您通过使用指向该成员的指针修改其成员之一(反之亦然),则该项目符号旨在防止缓存整个结构。请参阅 C 原理文档。
    • @JohannesSchaub-litb:严格的别名要求您使用与位置中的对象匹配的类型(此处为 int&amp;)。布局兼容性保证您正在查看正确的位置,即 p-&gt;*&amp;A::xp-&gt;*&amp;B::x 解析为相同的对象,相同的对齐方式。在枚举的情况下,布局兼容性再次保证了正确的位置和正确的对齐方式,但是你的类型错误。
    • @BenVoigt 请多多包涵,但我仍然不明白为什么在课堂案例中 BA 没有别名冲突。显然你是说别名分析不检查类类型是否匹配,而只检查被访问的成员。但如果是这样,为什么项目符号列表类?它说“一种类型是对象的动态类型的(可能是 cv 限定的)基类类型”——这显然涉及访问中的类类型。还是说a.b 不构成通过decltype(a) 类型的左值访问对象的值?
    • @JohannesSchaub-litb 项目符号列表是 C++ 最大的谜团之一。
    • 严格的别名规则对布局兼容的类没有例外,所以我不清楚你认为这如何回答这个问题。建议在答案中编辑更多解释。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-03-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-07-13
    • 2019-06-14
    • 2019-06-23
    相关资源
    最近更新 更多