{"id":129,"date":"2023-02-27T10:30:00","date_gmt":"2023-02-27T15:30:00","guid":{"rendered":"https:\/\/cyberbisson.com\/blog\/?p=129"},"modified":"2023-02-27T10:37:47","modified_gmt":"2023-02-27T15:37:47","slug":"when-empty-base-optimization-goes-wrong","status":"publish","type":"post","link":"https:\/\/cyberbisson.com\/blog\/2023\/02\/27\/when-empty-base-optimization-goes-wrong\/","title":{"rendered":"When Empty Base Optimization Goes \u201cWrong\u201d"},"content":{"rendered":"<div class=\"alignright right\">\n<img decoding=\"async\" src=\"\/editorial\/ebo.png\">\n<\/div>\n<p>Occasionally, we use C++ inheritance to \u201ctag\u201d a class for some later purpose, or perhaps to introduce a functional change without copying code (e.g., <tt><a href=\"https:\/\/www.boost.org\/doc\/libs\/1_81_0\/libs\/core\/doc\/html\/core\/noncopyable.html\">boost::noncopyable<\/a><\/tt>).  When the inherited class has no data, we (quite reasonably) expect to incur no run-time size overhead, due to <a href=\"https:\/\/en.cppreference.com\/w\/cpp\/language\/ebo\">Empty Base Optimization<\/a> (EBO), but there is one surprising case where we must take care, or unintentionally waste memory.  In this article, let\u2019s explore the issue, and how to guard against it.<\/p>\n<p><!--more--><\/p>\n\n<h1>The Problem<\/h1>\n<p>When you write a class with nothing in it, it is very surprising to do some compile-time check, and find that objects are paying a penalty for using that empty class.  The C++ Standard (C++20; \u00a76.7.2.8) defines what \u201cempty\u201d really means, but it\u2019s generally what you\u2019d expect: if a class has no fields and no virtual functions (ignoring a few other qualifications), you can pretty much expect it to be empty.  This means that when one derives from the class, the base should consume zero bytes of space \u2014 EBO \u2014 but there\u2019s a wrinkle (emphasis, mine):<\/p>\n<blockquote><p>\n  Two objects with overlapping lifetimes [\u2026] may have the same address if one is nested within the other, or if at least one is a subobject of zero size <strong>and they are of different types;<\/strong> otherwise, they have distinct addresses and occupy disjoint bytes of storage.<\/p><\/blockquote>\n<p>This verbiage is dense with meaning, but essentially, if one has a zero-length object at the start of a structure, and a zero-length object as the base class, <em>and<\/em> they are the same type, then the compiler must give space to the base class so it has a distinct memory address.  Consider an example:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"cpp\">struct empty_class { };\n\nstruct smaller_class : empty_class {\n    long long int m_x[2];\n};\n\nstruct bigger_class : empty_class {\n    smaller_class m_sc;\n};\n\nstruct biggest_class : empty_class {\n    bigger_class m_bc;\n    smaller_class m_sc;\n};\n<\/pre>\n<p>First, what are we <em>trying<\/em> to have the sizes of these objects be?  The obvious expectation is:<\/p>\n<ul>\n<li><tt>empty_class<\/tt>: zero objects, zero size (caveat: <tt>sizeof<\/tt> must indicate a size of 1 byte).<\/li>\n<li><tt>smaller_class<\/tt>: two 64-bit integers should be 16 bytes total.<\/li>\n<li><tt>bigger_class<\/tt>: since this just wraps <tt>smaller_class<\/tt>, it must be 16 bytes as well, right?<\/li>\n<li><tt>bigger_class<\/tt>: combining the first two classes, logically means this one is supposed to be 32 bytes.<\/li>\n<\/ul>\n<p>When we dump the AST (in this case from Clang<sup><a href=\"#ref_1\">1<\/a><\/sup>), we can see that some of the assumptions run into that paragraph from the Standard:<\/p>\n<pre>*** Dumping AST Record Layout\n         0 | struct empty_class (empty)\n           | [sizeof=1, dsize=1, align=1,\n           |  nvsize=1, nvalign=1]\n\n*** Dumping AST Record Layout\n         0 | struct smaller_class\n         0 |   struct empty_class (base) (empty)\n         0 |   long long[2] m_x\n           | [sizeof=16, dsize=16, align=8,\n           |  nvsize=16, nvalign=8]\n\n*** Dumping AST Record Layout\n         0 | struct bigger_class\n         0 |   struct empty_class (base) (empty)\n         8 |   struct smaller_class m_sc\n         8 |     struct empty_class (base) (empty)\n         8 |     long long[2] m_x\n           | [sizeof=24, dsize=24, align=8,\n           |  nvsize=24, nvalign=8]\n\n*** Dumping AST Record Layout\n         0 | struct biggest_class\n         0 |   struct empty_class (base) (empty)\n         8 |   struct bigger_class m_bc\n         8 |     struct empty_class (base) (empty)\n        16 |     struct smaller_class m_sc\n        16 |       struct empty_class (base) (empty)\n        16 |       long long[2] m_x\n        32 |   struct smaller_class m_sc\n        32 |     struct empty_class (base) (empty)\n        32 |     long long[2] m_x\n           | [sizeof=48, dsize=48, align=8,\n           |  nvsize=48, nvalign=8]\n<\/pre>\n<p>The classes, <tt>empty_class<\/tt> and <tt>smaller_class<\/tt> are 1 and 16 bytes (respectively), as expected.  Notice, however, that in the other two classes, we see <tt>empty_class<\/tt> as the base class, then the left column (the memory offset from the start of the structure) goes up to 8.  This is because the first field inherits again from <tt>empty_class<\/tt>.  It gets even worse for <tt>biggest_class<\/tt>, which retains the padding from <tt>bigger_class<\/tt>, and adds its own before it.  The size of <tt>bigger_class<\/tt> is 24 instead of 16, and <tt>biggest_class<\/tt> is 48 instead of 32!<\/p>\n<p>The point is this: objects in C++ are required to have their own distinct identity.  When you have two <tt>empty_class<\/tt> instances with two different names, the reasons are obvious: even though they have no data, one still needs to be able to differentiate between object #1 and object #2 with the equality operator.  Alas, for something like <tt>bigger_class<\/tt>, the base class doesn\u2019t have a distinct name, but it still has an <em>identity.<\/em>  Consider this example:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"cpp\">bigger_class o1;\nvoid f(empty_class *o);\n\nvoid g() {\n    empty_class *const p1    = &amp;o1;\n    empty_class *const p1_sc = &amp;o1.m_sc;\n    assert(p1 != p1_sc); \/\/ Fundamentally, this...\n\n    f(&amp;o1);\n    f(&amp;o1.m_sc);\n}\n<\/pre>\n<p>If the compiler does not give a different address for the base class, <tt>empty_class<\/tt> and for the base <tt>empty_class<\/tt> contained within <tt>m_sc<\/tt> (a <tt>smaller_class<\/tt> instance), then things start to break down.  The fundamental identity of <tt>o1<\/tt> and one of its fields would be identical!  Stranger still, any function like <tt>f()<\/tt> would not know which object it is using \u2014 probably not a big deal for an empty class, but we don\u2019t know if, for example, <tt>f()<\/tt> memoizes some result based on the identity of the objects it sees.<\/p>\n<h1>Just Make It Get Smaller, Please<\/h1>\n<p>Just to take the problem away, we could consider moving the actual data away from the empty base class.  Here, we set up implementation classes that we can use to define the other classes, noted by the \u201c<tt>_impl<\/tt>\u201d suffix.  They do not derive from <tt>empty_class<\/tt>, and therefore everything is exactly the size it&#8217;s supposed to be:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"cpp\">struct smaller_class_impl {\n    long long int m_x[2];\n};\nstruct smaller_class : empty_class, smaller_class_impl { };\n\nstruct bigger_class_impl {\n    smaller_class_impl m_sc;\n};\nstruct bigger_class : empty_class, bigger_class_impl { };\n\nstruct biggest_class_impl {\n    bigger_class_impl m_bc;\n    smaller_class_impl m_sc;\n};\nstruct biggest_class : empty_class, biggest_class_impl { };\n<\/pre>\n<p>To prove it, here\u2019s what Clang says:<\/p>\n<pre>*** Dumping AST Record Layout\n         0 | struct biggest_class\n         0 |   struct empty_class (base) (empty)\n         0 |   struct biggest_class_impl (base)\n         0 |     struct bigger_class_impl m_bc\n         0 |       struct smaller_class_impl m_sc\n         0 |         long long[2] m_x\n        16 |     struct smaller_class_impl m_sc\n        16 |       long long[2] m_x\n           | [sizeof=32, dsize=32, align=8,\n           |  nvsize=32, nvalign=8]\n<\/pre>\n<p>Unfortunately, this actually makes life pretty complicated for an interface of any complexity.  Consider if the interface to <tt>bigger_class<\/tt> had a function that returned an instance of <tt>bigger_class<\/tt> \u2014 but now everything has to live in <tt>bigger_class_impl<\/tt> \u2014 should it return the tagged class, or the implementation class?  Things have to be forward-declared, perhaps, but then it has to be separate if it\u2019s <tt>inline<\/tt>.  Or should it be in <tt>bigger_class<\/tt>, but then it\u2019s separate from the data in <tt>_impl<\/tt>.  Yuck!  This is certainly not a drop-in fix for such a code-base.<\/p>\n<p>So while this approach does confirm out reasoning \u2014 that EBO is indeed affected by the repeated base class \u2014 we can do better.<\/p>\n<h1>A Much Better Answer<\/h1>\n<p>The solution I propose here is painfully simple.  <em>Make the empty base class a class template.<\/em>  If the problem is that the same address for two objects cannot have the same type, let\u2019s change the type so that it\u2019s based on the most-derived type!<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"cpp\">template&lt;typename T&gt; struct empty_class { };\n\nstruct smaller_class : empty_class&lt;smaller_class&gt; {\n    long long int m_x[2];\n};\n\nstruct bigger_class : empty_class&lt;bigger_class&gt; {\n    smaller_class m_sc;\n};\n\nstruct biggest_class : empty_class&lt;biggest_class&gt; {\n    bigger_class m_bc;\n    smaller_class m_sc;\n};\n<\/pre>\n<p>The empty base class came from a class <em>template,<\/em> but a template isn&#8217;t a \u201creal\u201d type until it is instantiated.  By handing the derived class to the template, the template can also perhaps include special functions for specific classes, but this is probably not the point.  The point is that at compile time, it\u2019s still pretty easy to check if a class derives from <tt>empty_class&lt;T&gt;<\/tt> for any <tt>T<\/tt>.  Meanwhile, <tt>is_same&lt;empty_class&lt;bigger_class&gt;, empty_class&lt;biggest_class&gt;&gt;<\/tt> is <tt>false<\/tt>, and the compiler doesn\u2019t need to provide a unique address for adjacent instances.  Problem solved!<\/p>\n<pre>*** Dumping AST Record Layout\n         0 | struct bigger_class\n         0 |   struct empty_class&lt;struct bigger_class&gt; (base) (empty)\n         0 |   struct smaller_class m_sc\n         0 |     struct empty_class&lt;struct smaller_class&gt; (base) (empty)\n         0 |     long long[2] m_x\n           | [sizeof=16, dsize=16, align=8,\n           |  nvsize=16, nvalign=8]\n\n*** Dumping AST Record Layout\n         0 | struct biggest_class\n         0 |   struct empty_class&lt;struct biggest_class&gt; (base) (empty)\n         0 |   struct bigger_class m_bc\n         0 |     struct empty_class&lt;struct bigger_class&gt; (base) (empty)\n         0 |     struct smaller_class m_sc\n         0 |       struct empty_class&lt;struct smaller_class&gt; (base) (empty)\n         0 |       long long[2] m_x\n        16 |   struct smaller_class m_sc\n        16 |     struct empty_class&lt;struct smaller_class&gt; (base) (empty)\n        16 |     long long[2] m_x\n           | [sizeof=32, dsize=32, align=8,\n           |  nvsize=32, nvalign=8]\n<\/pre>\n<p>Revisiting the \u201cidentity\u201d example above, the <tt>assert<\/tt> no longer compiles because <tt>empty_class<\/tt> is a class template, and if you provide the correct template parameters so that it accepts the provided addresses, it then complains about comparing distinct pointer types.  The function, <tt>f()<\/tt> now must be a function template, and the two invocations in the example call two distinct functions, with no possibility of confusing the identities of the class and its member data.<\/p>\n<h1>Final Thoughts<\/h1>\n<p>This kind of problem is one that you might believe is working as expected for a years before realizing it\u2019s not quite right.  It\u2019s not (generally) a serious issue, as we\u2019re talking about bytes, but the lost data is completely useless and wasted in many cases.  I offer the following suggestions:<\/p>\n<ol>\n<li>If it\u2019s at all reasonable, validate the size of your class with <tt>static_assert<\/tt>.  For a class with a lot of data, or something algorithmic and frequently changing, this can be tedious and quite unnecessary.  For a data class (think, a UUID class), however, you probably expect a specific size by design.  Validate it.<\/li>\n<li>Check out things that people tend to add to classes to adjust the interface \u2014 like <tt>boost::noncopyable<\/tt>.  They may not be as harmless as they seem.<\/li>\n<li>Change any \u201ctag\u201d classes in the code, and convert them to a template when reasonable.  Perhaps, too, by using <tt><a href=\"https:\/\/en.cppreference.com\/w\/cpp\/types\/enable_if\">std::enable_if<\/a><\/tt>, the tag is not even needed.<\/li>\n<\/ol>\n<p><center><\/p>\n<hr width=\"75%\">\n<p><\/center><\/p>\n<h1>Notes<\/h1>\n<ol>\n<li><a name=\"ref_1\"><\/a>This is pretty easy in Clang: just invoke \u201c<tt>clang++ -cc1 -emit-llvm -fdump-record-layouts from.cpp &gt; dest.log<\/tt>\u201d.  This will work for C or C++.  With GCC, you can get similar output with the <tt><a href=\"https:\/\/gcc.gnu.org\/onlinedocs\/gcc\/Developer-Options.html\">-fdump-lang-all<\/a><\/tt> flag, but it only applies to C++.  For Clang, if you have code of any complexity, you probably need to pre-process it first, then analyze the pre-processed file.  The \u201cCC1\u201d phase of compilation does not generally even know where to find system headers by default.<\/li>\n<\/ol>\n<hr>\n<p>Enjoy a <a href=\"\/download\/EBO_Goes_Wrong.pdf\">printable version here<\/a>!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Occasionally, we use C++ inheritance to \u201ctag\u201d a class for some later purpose, or perhaps to introduce a functional change without copying code (e.g., boost::noncopyable). When the inherited class has no data, we (quite reasonably) expect to incur no run-time size overhead, due to Empty Base Optimization (EBO), but there is one surprising case where &hellip; <a href=\"https:\/\/cyberbisson.com\/blog\/2023\/02\/27\/when-empty-base-optimization-goes-wrong\/\" class=\"more-link\">Continue reading<span class=\"screen-reader-text\"> &#8220;When Empty Base Optimization Goes \u201cWrong\u201d&#8221;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[35],"tags":[36,47,48,49,50,39,12],"class_list":["post-129","post","type-post","status-publish","format-standard","hentry","category-c","tag-c","tag-ebo","tag-inheritance","tag-memory","tag-optimization","tag-programming","tag-software"],"_links":{"self":[{"href":"https:\/\/cyberbisson.com\/blog\/wp-json\/wp\/v2\/posts\/129","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/cyberbisson.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/cyberbisson.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/cyberbisson.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/cyberbisson.com\/blog\/wp-json\/wp\/v2\/comments?post=129"}],"version-history":[{"count":10,"href":"https:\/\/cyberbisson.com\/blog\/wp-json\/wp\/v2\/posts\/129\/revisions"}],"predecessor-version":[{"id":150,"href":"https:\/\/cyberbisson.com\/blog\/wp-json\/wp\/v2\/posts\/129\/revisions\/150"}],"wp:attachment":[{"href":"https:\/\/cyberbisson.com\/blog\/wp-json\/wp\/v2\/media?parent=129"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/cyberbisson.com\/blog\/wp-json\/wp\/v2\/categories?post=129"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/cyberbisson.com\/blog\/wp-json\/wp\/v2\/tags?post=129"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}