空数据成员优化:是否可能?

时间:2011-01-07 09:08:08

标签: c++ compiler-optimization

在C ++中,大多数优化都是从as-if规则派生的。也就是说,只要程序表现得如果没有进行优化,那么它们就是有效的。

空基优化就是这样一个技巧:在某些情况下,如果基类为空(没有任何非静态数据成员),则编译器可能会忽略其内存表示。

显然,似乎标准禁止对数据成员进行优化,即使数据成员为空,它仍然必须至少占用一个字节的位置:来自n3225, [class]

  

4 - 类类型的完整对象和成员子对象应具有非零大小。

注意:这会导致使用私有继承进行策略设计,以便在适当时启动EBO

我想知道,如果使用as-if规则,仍然可以执行此优化。


编辑:遵循一些答案和评论,并让我更清楚我想知道的事情。

首先,我举一个例子:

struct Empty {};

struct Foo { Empty e; int i; };

我的问题是,为什么sizeof(Foo) != sizeof(int)?特别是,除非你指定一些打包,否则很可能是因为对齐问题,Foo将是int的两倍,这似乎是荒谬的膨胀。

注意:我的问题不是为什么sizeof(Foo) != 0,EBO实际上并不需要

根据C ++,这是因为没有子对象可能具有零大小。但是,基数被授权具有零大小(EBO):

struct Bar: Empty { int i; };

很可能(感谢EBO)服从sizeof(Bar) == sizeof(int)

Steve Jessop 似乎认为没有两个子对象具有相同的地址。我考虑过它,但在大多数情况下它实际上并没有阻止优化:

如果您有“未使用”的内存,那么它很简单:

struct UnusedPadding { Empty e; Empty f; double d; int i; };
// chances are that the layout will leave some memory after int

但事实上,它甚至比那更“糟糕”,因为Empty空间从未被写入(如果EBO开始......你最好不要这样做),因此你实际上可以把它放在被占用的位置不是另一个对象地址的地方:

struct Virtual { virtual ~Virtual() {} Empty e; Empty f; int i; };
// most compilers will reserve some space for a virtual pointer!

或者,即使在我们原来的情况下:

struct Foo { Empty e; int i; }; // deja vu!

如果我们想要的只是不同的地址,那么可以(char*)foo.e == (char*)foo.i + 1

5 个答案:

答案 0 :(得分:6)

在as-if规则下:

struct A {
    EmptyThing x;
    int y;
};

A a;
assert((void*)&(a.x) != (void*)&(a.y));

不得触发断言。因此,当你只需要在结构中添加填充时,我没有看到秘密制作x大小为0的任何好处。

我认为理论上编译器可以跟踪是否可以将指针带到成员,并且只有在它们肯定不是时才进行优化。这将是有限的用途,因为有两个不同版本的结构具有不同的布局:一个用于优化的情况,一个用于通用代码。

但是,例如,如果您在堆栈上创建A的实例,并使用完全内联的(或优化器可见的)执行某些操作,是的,可以完全省略结构的某些部分。这不是特定于空对象的 - 空对象只是一个对象的特殊情况,它的存储空间不被访问,因此在某些情况下根本不会被分配。

答案 1 :(得分:2)

由于技术原因,C ++要求空类的大小应为非零 这是为了强制不同的对象具有不同的内存地址。因此编译器会静默地将一个字节插入“空”对象中 此约束不适用于派生类的基类部分,因为它们不是独立的。

答案 2 :(得分:2)

它来自带有[[no_unique_address]]属性的c ++ 20。

提案P0840r2accepted into the draft standard。它有这个例子:

template<typename Key, typename Value, typename Hash, typename Pred, typename Allocator>
class hash_map {
  [[no_unique_address]] Hash hasher;
  [[no_unique_address]] Pred pred;
  [[no_unique_address]] Allocator alloc;
  Bucket *buckets;
  // ...
public:
  // ...
};

答案 3 :(得分:1)

由于Empty是POD类型,因此您可以使用memcpy覆盖其“表示”,因此最好不要将其与其他C ++对象或有用数据共享。

答案 4 :(得分:-1)

考虑struct Empty { };,考虑sizeof(Empty) == 0会发生什么。为Empty对象分配堆的通用代码可能很容易表现不同,例如 - realloc(p, n * sizeof(T))TEmpty,等同于free(p)。如果sizeof(Empty) != 0那么像memset / memcpy等会尝试处理Empty对象未使用的内存区域。因此,编译器需要根据值的最终用法来拼接像sizeof(Empty)这样的东西 - 听起来对我来说几乎是不可能的。

另外,根据当前的C ++规则,每个成员具有不同地址的保证意味着您可以使用这些地址来编码关于这些字段的某些状态 - 例如文本字段名称,是否应该访问字段对象的某个成员函数等。如果地址突然重合,任何依赖这些键的现有代码都可能会中断。