【问题标题】:When do programmers use Empty Base Optimization (EBO)程序员何时使用空基优化 (EBO)
【发布时间】:2011-05-18 12:44:02
【问题描述】:

我正在阅读有关空基优化 (EBO) 的信息。在阅读的过程中,我的脑海中突然出现了以下问题:

  1. 对派生类没有任何贡献(无论是功能方面还是数据方面),使用 Empty 类作为基类有什么意义?

  2. this article,我读到了这个:

//S 为空
类结构 T : S
{
整数 x;
};

[...]

请注意,我们没有丢失任何数据或 代码准确性:当您创建一个 S 类型的独立对象, 对象的大小仍为 1(或更多)为 前;仅当 S 用作基础时 另一个类的类做它的记忆 足迹缩小到零。意识到 这种节省的影响,想象一下 包含 125,000 个的向量 对象。 仅 EBO 就节省了一半 兆字节的内存!

这是否意味着如果我们不使用“S”作为“T”的基类,我们将必然消耗两倍兆字节的内存?我认为,这篇文章比较了两种我认为不正确的不同场景。

我想知道当 EBO 被证明有用时的真实场景。(意味着,在同样的场景中,如果我们不使用 EBO,我们必然会不知所措!)。

请注意,如果您的回答包含这样的解释:

重点是一个空类的大小不为零,但是当派生或派生它时,它的大小可以为零,那么我不是在问这个,因为我已经知道了。我的问题是,为什么有人首先会从一个空类中派生出他的类? 即使他没有派生而只是编写他的类(没有任何空基),他是否会不知所措?

【问题讨论】:

  • @Suma...你在我的帖子中做了什么改变? :-/.. 我想不通..
  • 为什么说基类在功能方面没有任何贡献?
  • @jalf .... 到目前为止我还没有看到任何示例.. 顺便说一下,我所说的功能是指做某事而不是什么都不做的函数
  • @Nawaz:但是为什么基类函数不能做“做某事”呢?
  • 空类并不意味着没有“成员”,而是意味着“没有数据成员”。它仍然可以有“功能成员”。因此,您可能需要该功能,而重用该功能的最简单方法是通过组合。但是,如果您怀疑对象可能没有数据成员,您可能更愿意为此类类型私有继承以减少派生类的内存占用。这不是关于“我会从 A 继承,因为它对 A 有好处”,而是“我更喜欢继承而不是组合以从 EBO 中受益,因为无论如何我都需要 B 中的对象 A”。

标签: c++ class optimization memory-management compiler-optimization


【解决方案1】:

EBO 在policy based design 的上下文中很重要,您通常从多个策略类私有继承。如果我们以线程安全策略为例,可以想象一下伪代码:

class MTSafePolicy
{
public:
  void lock() { mutex_.lock(); }
  void unlock() { mutex_.unlock(); }

private:
  Mutex mutex_;
};

class MTUnsafePolicy
{
public:
  void lock() { /* no-op */ }
  void unlock() { /* no-op */ }
};

给定一个基于策略的设计类,例如:

template<class ThreadSafetyPolicy>
class Test : ThreadSafetyPolicy
{
  /* ... */
};

使用带有MTUnsafePolicy 的类只需在Test 类的开销上不增加任何大小:这是不为不使用的东西付费的完美示例。

【讨论】:

  • 你是说在这个例子中,类MTSafePolicyMTUnsafePolicy类似于Java中的接口?既然如此,为什么不把它标记为抽象,以防止实例化。
【解决方案2】:

EBO 并不是真正的优化(至少不是您在代码中所做的优化)。重点是空类的大小不为零,但在派生或派生时,它的大小可以为零。

这是最常见的结果:

class A { };
class B { };

class C { };
class D : C { };

#include <iostream>
using namespace std;

int main()
{
        cout << "sizeof(A) + sizeof(B) == " << sizeof(A)+sizeof(B) << endl;
        cout << "sizeof(D) == " << sizeof(D) << endl;

        return 0;
}

输出:

sizeof(A) + sizeof(B) == 2
sizeof(D) == 1

编辑: 优化是,如果您确实派生(例如从函子或仅具有静态成员的类),则类的大小(即派生)不会增加 1(或更可能是 4 或8 由于填充字节)。

【讨论】:

  • @Let_Me_Be... 我已经编辑了我的问题.. 请再看一遍.. 我希望你现在明白我的要求了。 :-)
  • @Let_Me_Be...感谢这次更新..现在说得通了..我正在寻找更多回应...更多场景..
  • @Nawaz 你所说的场景到底是什么意思?
  • @Nawaz 所以你想知道如何才能有一个空班吗?好吧,它不能包含任何非静态属性或虚函数。使用选项几乎是无限的。例如:朋友注入,或者通常是 Curiously Recurring Pattern 的大多数其他巧妙用途(如实例计数器)。
  • @Let_Me_Be ..顺便说一句,从具有“仅”静态成员的类派生您的类有什么好处?我看不出任何意义:-/
【解决方案3】:

EBO 中的“优化”是指使用基类的情况可以被优化为使用比使用相同类型的成员更少的内存。 IE。你比较

struct T : S 
{
      int x;
};

struct T
{
      S s;
      int x;
};

没有

struct T
{
      int x;
};

如果你的问题是你为什么会有一个空类(作为一个成员,或者作为一个基类),那是因为你使用了它的成员函数。空意味着它没有数据成员,而不是它根本没有任何成员。使用模板进行编程时经常会执行此类操作,其中基类有时是“空的”(没有数据成员),有时不是。

【讨论】:

    【解决方案4】:

    当程序员想要在不增加客户端类大小的情况下向客户端公开一些数据时使用它。空类可以包含枚举和 typedef 或一些客户端可以使用的定义。使用它的类的最明智的方法是,私下继承这样的类。这将从外部隐藏数据,并且不会增加您的班级规模。

    【讨论】:

      【解决方案5】:

      可以有没有任何成员变量的空类,但是可以充当实用程序类的成员函数(staticnon static),我们称之为EmptyClass。现在我们可以有一个案例,我们想要创建一个类(我们称之为SomeClass),它与EmptyClass 具有包含类型的关系,但不是“is-a”关系。一种方法是在SomeClass 中创建EmptyClass 类型的成员对象,如下所示:

      class EmptyClass  
      {
      public:
          void someFun1();
          static int someUtilityFun2();
      };
      //sizeof(EmptyClass) = 1
      
      
      class SomeClass
      {
       private:
          EmptyClass e;
          int x;
      };
      //sizeof(SomeClass) = 8
      

      现在,由于某些对齐要求,编译器可能会向SomeClass 添加填充,其大小现在为 8 字节。更好的解决方案是让SomeClassEmptyClass 私下派生,这样SomeClass 将可以访问EmptyClass 的所有成员函数,并且不会通过填充增加额外的大小。

      class SomeClass : private EmptyClass
      {
      private:
          int x;
      } 
      //sizeof(SomeClass) = 4
      

      【讨论】:

        【解决方案6】:

        大多数时候,空基类要么被多态地使用(文章提到),作为“标记”类,要么作为异常类(尽管这些通常派生自非空的 std::exception) .有时有充分的理由开发以空基类开头的类层次结构。

        Boost.CompressedPair 使用 EBO 在元素之一为空的情况下缩小对象的大小。

        【讨论】:

        • 不知道为什么这被否决了,对我来说似乎很好。
        【解决方案7】:

        EASTL 很好地解释了他们为什么需要 EBO,在他们链接到/信用的论文中也有深入的解释

        【讨论】:

        • ...这篇文章太大了...有时间我会看的..然后我会评论它...:-)...谢谢顺便说一句。
        • 文章中的一些引用可以方便地防止链接失效(并使其成为真正的答案,而不仅仅是链接)
        【解决方案8】:

        EBO 不受程序员影响,和/或如果程序员选择不从空基类派生,他将受到惩罚。

        编译器控制是否:

        class X : emptyBase { int X; };
        class Y { int x };
        

        你得到sizeof(X) == sizeof(Y) 与否。如果你这样做,编译器会实现 EBO,如果不是,它不会。

        绝不会有sizeof(Y) &gt; sizeof(X)会发生的情况。

        【讨论】:

          【解决方案9】:

          我能想到的主要好处是dynamic_cast。您可以获取指向 S 的指针并尝试将其动态转换为从 S 继承的任何内容 - 假设 S 提供了像虚拟析构函数这样的虚函数,它几乎必须作为基类来做。例如,如果您正在实现一种动态类型语言,您可能希望或需要每个类型都从基类派生,纯粹是为了类型擦除存储和通过 dynamic_cast 进行类型检查。

          【讨论】:

          • .. 如果 S 有虚函数.. 那么很可能它不是一个空类!
          • @Nawez:为什么不呢?基类 S 很可能提供无操作实现,甚至是纯虚拟实现。看看icecrime的回答;可以说lock 应该是虚拟的。
          • @MSalters...如果一个类具有虚函数,这意味着它的大小不能为零(至少对于现实生活中的编译器).. 至于icecrime 的答案,非虚拟锁可以完美地工作!
          • @Nawaz:任何班级都不能有sizeof(Class)==0,无论是否为空。但是我们专门讨论的是空基类子对象的大小。它不需要自己的 vtable,也不需要 vtable 指针。假设一个 vtable 指针在偏移量 0 处的通用布局;这将导致零大小的基类子对象与派生类共享其 vtable 指针。没问题:无论如何,它们应该是相同的,这几乎就是虚函数的意义所在。
          猜你喜欢
          • 2021-11-07
          • 2020-04-26
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-04-01
          相关资源
          最近更新 更多