开发者

When do programmers use Empty Base Optimization (EBO)

开发者 https://www.devze.com 2023-01-28 00:31 出处:网络
I was reading about Empty Base Optimization(EBO). While reading, the following questions popped up in my mind:

I was reading about Empty Base Optimization(EBO). While reading, the following questions popped up in my mind:

  1. What is the point of using Empty class as base class when it contributes nothing to the derived classes (neither functionality-wise, nor data-wise)?

  2. In this article, I read this:

//S is empty

class struct T : S

{

      int x;

};

[...]

Notice that we didn’t lose any data or code accuracy: when you create a standalone object of type S, the object’s size is still 1 (or more) as before; only when S is used as base class of another class does its memory footprint shrink to zero. To realize the impact of this saving, imagine a vector that contains 125,000 objects. The EBO alone saves half a megabyte of memory!

Does it mean that if we don't use "S" as base class of "T", we would necessarily consume double of megabyte of memory? I think, the article compares two different scenarios which I don't think is correct.

I would like to know a real scenario when EBO can proven to be useful.(means, in the same scenario, we would necessarily be at loss IF we don't use EBO!).

Please note that if your answer contains explanations like this :

The whole point is that an empty class has non-zero size, but when derived or deriving it can have zero size, then I'm NOT asking that, as I know that already. M开发者_开发百科y question is, why would anyone derive his class from an empty class in the first place? Even if he doesn't derive and simply writes his class (without any empty base), is he at loss in ANY way?


EBO is important in the context of policy based design, where you generally inherit privately from multiple policy classes. If we take the example of a thread safety policy, one could imagine the pseudo-code :

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 */ }
};

Given a policy based-design class such as :

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

Using the class with a MTUnsafePolicy simply add no size overhead the class Test : it's a perfect example of don't pay for what you don't use.


EBO isn't really an optimization (at least not one that you do in the code). The whole point is that an empty class has non-zero size, but when derived or deriving it can have zero size.

This is the most usual result:

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;
}

Output:

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

To the edit: The optimization is, that if you actually do derive (for example from a functor, or from a class that has only static members), the size of your class (that is deriving) won't increase by 1 (or more likely 4 or 8 due to padding bytes).


The "Optimization" in the EBO means the case when you use base class can be optimized to use less memory than if you use a member of the same type. I.e. you compare

struct T : S 
{
      int x;
};

with

struct T
{
      S s;
      int x;
};

not with

struct T
{
      int x;
};

If your question is why would you have an empty class at all (either as a member, or as a base), it is because you use its member functions. Empty means it has no data member, not that it does not have any members at all. Things like this are often done when programming with templates, where the base class is sometimes "empty" (no data members) and sometimes not.


Its used when programmers want to expose some data to client without increasing the client class size. The empty class can contain enums and typedefs or some defines which the client can use.The most judicious way to use such a class it it to,inherit such a class privately. This will hide the data from outside and wil not increase your class size.


There can be empty classes which do not have any member variables, but member functions (static or non static) which can act as utility classes, lets call this EmptyClass. Now we can have a case where we want to create a class (let's call it SomeClass) which have a containment kind of relation with EmptyClass, but not 'is-a' relation. One way is to create a member object of type EmptyClass in SomeClass as follows:

class EmptyClass  
{
public:
    void someFun1();
    static int someUtilityFun2();
};
//sizeof(EmptyClass) = 1


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

Now due to some alignment requirements compilers may add padding to SomeClass and its size is now 8 bytes. The better solution is to have a SomeClass derive privately from EmptyClass and in this way SomeClass will have access to all member functions of EmptyClass and won't increase the extra size by padding.

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


Most of the time, an empty base class is either used polymorphically (which the article mentions), as "tag" classes, or as exception classes (although those are usually derived from std::exception, which is not empty). Sometimes there is a good reason to develop a class hierarchy which begins with an empty base class.

Boost.CompressedPair uses the EBO to shrink the size of objects in the event that one of the elements is empty.


EASTL has a good explanation as to why they needed EBO, its also explained in-depth in the paper they link to/credit


EBO is not something the programmer influences, and/or the programmer would be punished for if (s)he chose not to derive from an empty base class.

The compiler controls whether for:

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

you get sizeof(X) == sizeof(Y) or not. If you do, the compiler implements EBO, if not, it doesn't.

There never is any situation where sizeof(Y) > sizeof(X) would occur.


The primary benefit I can think of is dynamic_cast. You can take a pointer to S and attempt to dynamic_cast it to anything that inherits from S- assuming that S offers a virtual function like a virtual destructor, which it pretty much must do as a base class. If you were, say, implementing a dynamically typed language, you may well wish or need for every type to derive from a base class purely for the purposes of type-erased storage, and type checking through dynamic_cast.

0

精彩评论

暂无评论...
验证码 换一张
取 消