【问题标题】:Empty Data Member Optimization: would it be possible?空数据成员优化:有可能吗?
【发布时间】:2011-06-05 04:03:39
【问题描述】:

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

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

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

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

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

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


编辑:遵循一些答案和 cmets,以便更清楚地了解我在想什么。

首先,我举个例子:

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

【问题讨论】:

  • 看看 Boost 的 Compressed Pair 库,看看如何获​​得这种优化。
  • @GMan:他们巧妙地使用了 EBO。但实际上 EBO 的这种使用正是促使我提出问题的原因。
  • 从 9.2.12 开始“以后的成员在类对象中有更高的地址”,所以你的 (char*)foo.e == (char*)foo.i + 1 不太有效;-P
  • “如果我们想要的只是不同的地址” - 我认为我们不仅想要不同的地址,我们还想要不重叠的对象。其中“我们”是标准委员会。我认为您的虚拟案例是可以的,只要将空对象定义为虚拟对象的开头,或者在类中的另一个位置,其中的字节还不是任何子对象的一部分。您也可以在空成员的任何一侧都有访问说明符的地方玩这种技巧,因为那样它就不必与其他成员按顺序排列。

标签: c++ compiler-optimization


【解决方案1】:

c++20 将带有 [[no_unique_address]] 属性。

提案P0840r2 一直是accepted 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:
  // ...
};

【讨论】:

【解决方案2】:

在as-if规则下:

struct A {
    EmptyThing x;
    int y;
};

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

不得触发断言。所以我看不出秘密让 x 的大小为 0 有什么好处,而无论如何你只需要在结构中添加填充。

我想理论上编译器可以跟踪是否可以将指针指向成员,并且仅在它们肯定不是时才进行优化。这将具有有限的用途,因为会有两种不同版本的结构具有不同的布局:一种用于优化案例,另一种用于通用代码。

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

【讨论】:

  • 是的,这个地址身份是真正让我恼火的地方。虽然我可以猜到它可能有用,但未获取地址的对象相当于未使用的对象(因为在其上调用任何方法都需要它的地址),我不知道 C++ 编译器如何实现对象是除了所有内联类之外未使用。你知道为什么这个非零用于 吗? (即如果它是 C 的残余或仍然以更现代的风格使用)
  • @Matthieu:好吧,我想完整对象的非零大小是这样你可以做sizeof(array)/sizeof(*array)而不是除以零。我想不出成员子对象的非零大小的有力理由 - 如果您需要一些具有不同地址的成员对象,您(程序员)可以确保它们的类型为 char 而不是 EmptyThing .我猜对象/内存模型中的某个地方需要特殊情况处理,并且假设成员子对象是不同的且不重叠的会更简单。
  • 我已经稍微更新了我的问题。我不需要完整对象的非零大小(尽管这也可以很好,允许大量其他优化,并且sizeof 也是编译时代码,所以无论如何不用担心在运行时除以零异常) .我不明白为什么如果我们可以拥有 EBO(这是一个零大小的基类),我们就不能拥有 EDMO(这是一个零大小的数据成员)。
  • @Matthieu:如果您将问题更改为“是否可以更改标准以允许 EDMO 没有重大连锁反应”,那么我认为答案是“是的,除非它会破坏现有代码假设 EDMO 具有不同的地址”。您最初的问题是编译器是否可以根据 as-if 规则在没有标准明确许可的情况下实现 EDMO。答案仍然是“大部分没有”。
  • @MatthieuM。 “除非完全内联,否则无法猜测是否将使用数据成员的地址”,并且您可能不希望类的表示在某些成员函数实现发生更改时发生更改!
【解决方案3】:

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

【讨论】:

    【解决方案4】:

    因为Empty是一个POD类型,你可以使用memcpy覆盖它的“表示”,所以最好不要与另一个C++对象或有用的数据共享它。

    【讨论】:

      【解决方案5】:

      给定struct Empty { }; 考虑如果sizeof(Empty) == 0 会发生什么。为空对象分配堆的通用代码很容易表现出不同的行为,例如,realloc(p, n * sizeof(T)),其中TEmpty,然后等效于free(p)。如果sizeof(Empty) != 0 那么memset/memcpy 等将尝试在Empty 对象未使用的内存区域上工作。因此,编译器需要根据值的最终使用情况来拼接 sizeof(Empty) 之类的东西——这对我来说听起来几乎是不可能的。

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

      【讨论】:

      • "诸如 memset/memcpy 之类的东西会尝试在 Empty 对象未使用的内存区域上工作" - 仅当您将错误(即非 0)长度传递给memset/memcpy.
      • @Tony:我并不是建议 sizeof(Empty)0,而是给定 struct Foo { Empty e; int i; }; 一个 sizeof(Foo) == sizeof(int) 这意味着在这里节省大约 4/8 字节的内存(因为对齐)
      • @Steve: 正是:-) - 该句子以“sizeof(Empty) != 0”开头,这意味着当您使用sizeof(T) 计算 memset/memcpy 调用的大小时,其中 T 是空,你会得到一个非零值......
      • @Matthieu: 所以sizeof(Empty) != sizeof Foo.e, sizeof(Foo) &lt; sizeof Foo.e + sizeof Foo.i...? :-) 我要说的是我认为使用sizeof 的模板代码有太多的极端情况(也许大多数涉及C 风格的指针hackery、memset、malloc/realloc 等,而不是仅C++ 的等价物-但它们必须继续按预期工作)。
      • @Tony: sizeof(Foo) != sizeof(Foo.e) + sizeof(Foo.i)sizeof(Foo.e) 独立于eFoo 中的内存表示,毕竟它并不表示例如e 后面的填充。此外,对于EBO,您已经拥有sizeof(Bar) &lt; sizeof(Empty) + sizeof(Bar.i)。我不确定指针hackery参数,我不太关心alloc(你不要在subjobjects上使用malloc/realloc)和memset/memcpy使用是危险的:如果它改为基类怎么办数据成员?然后EBO可能会开始......
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-03-19
      • 1970-01-01
      • 1970-01-01
      • 2012-07-26
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多