Optimizing the Layout of Empty Base Classes in VS2015 Update 2

Vinny Romano

The C++ Standard has only a handful of requirements regarding how a class is laid out in memory, one of which is that the size of a most derived object shall have a non-zero size and shall occupy one or more bytes of storage. Because this requirement only extends to most derived objects, base class subobjects are not subjected to that constraint. Taking advantage of this liberty in the standard is commonly referred to as the Empty Base Class Optimization (EBCO), and results in reduced memory consumption, which can improve performance. The Visual C++ compiler has historically had limited support for EBCO; however, in Visual Studio 2015 Update 2, we have added a new __declspec(empty_bases) attribute for class types that takes full advantage of this optimization.

In Visual Studio 2015, barring any __declspec(align()) or alignas() specifications, an empty class is 1 byte in size:

struct Empty1 {};
static_assert(sizeof(Empty1) == 1, "Empty1 should be 1 byte");

A class with a single non-static data member of type char is also 1 byte in size:

struct Struct1
{
  char c;
};
static_assert(sizeof(Struct1) == 1, "Struct1 should be 1 byte");

Combining these classes in a class hierarchy also results in a class that is 1 byte in size:

struct Derived1 : Empty1
{
  char c;
};
static_assert(sizeof(Derived1) == 1, "Derived1 should be 1 byte");

This is the Empty Base Class Optimization at work, as without it Derived1 would be 2 bytes in size, 1 byte for Empty1 and 1 byte for Derived1::c. The class layout is also optimal when there is a chain of empty classes:

struct Empty2 : Empty1 {};
struct Derived2 : Empty2
{
  char c;
};
static_assert(sizeof(Derived2) == 1, "Derived2 should be 1 byte");

However, the default class layout in Visual Studio 2015 does not take advantage of EBCO in multiple inheritance scenarios:

struct Empty3 {};
struct Derived3 : Empty2, Empty3
{
  char c;
};
static_assert(sizeof(Derived3) == 1, "Derived3 should be 1 byte"); // Error

Although Derived3 could be 1 byte in size, the default class layout results in it being 2 bytes in size. The class layout algorithm is adding 1 byte of padding between any two consecutive empty base classes, effectively resulting in Empty2 consuming an extra byte within Derived3:

class Derived3  size(2):
   +---
0  | +--- (base class Empty2)
0  | | +--- (base class Empty1)
   | | +---
   | +---
1  | +--- (base class Empty3)
   | +---
1  | c
   +---

The effects of this suboptimal layout are compounded when the alignment requirements of a subsequent base class or member subobject necessitate additional padding:

struct Derived4 : Empty2, Empty3
{
  int i;
};
static_assert(sizeof(Derived4) == 4, "Derived4 should be 4 bytes"); // Error

The natural alignment for an object of type int is 4 bytes, so an additional 3 bytes of padding need to be added after Empty3 to correctly align Derived4::i:

class Derived4 size(8):
   +---
0  | +--- (base class Empty2)
0  | | +--- (base class Empty1)
   | | +---
   | +---
1  | +--- (base class Empty3)
   | +---
   | <alignment member> (size=3)
4  | i
   +---

Another issue with the default class layout in Visual Studio 2015 is that an empty base class may be laid out at an offset past the end of the class:

struct Struct2 : Struct1, Empty1
{
};
static_assert(sizeof(Struct2) == 1, "Struct2 should be 1 byte");
class Struct2 size(1):
   +---
0  | +--- (base class Struct1)
0  | | c
   | +---
1  | +--- (base class Empty1)
   | +---
   +---

Although Struct2 is the optimal size, Empty1 is laid out at offset 1 within Struct2 but the size of Struct2 is not increased to account for it. As a result, for an array A of Struct2 objects, the address of the Empty1 subobject of A[0] will be the same as the address of A[1], which should not be the case. This issue would not occur if Empty1 were laid out at offset 0 within Struct2, thereby overlapping the Struct1 subobject. It would be great if the default layout algorithm could be modified to address these limitations and fully take advantage of EBCO; however, such a change can’t be made in an Update release of Visual Studio 2015. One of the requirements of an Update release is that object files and libraries built with the initial release of Visual Studio 2015 continue to be compatible with those built with future Update releases. If the default layout for a class were to change as a result of EBCO, every object file and library that contains the class definition would need to be re-compiled so they all agree on the class layout. This would also extend to libraries obtained from external sources, which would require the developer of such libraries to provide independent versions that are compiled with and without the EBCO layout so they can support customers who aren’t compiling with the latest Update release. Although we can’t change the default layout, we can provide a means to change the layout on a per-class basis, and this is what we have done in Visual Studio 2015 Update 2 with the addition of the __declspec(empty_bases) class attribute. A class defined with this attribute will make full use of EBCO.

struct __declspec(empty_bases) Derived3 : Empty2, Empty3
{
  char c;
};
static_assert(sizeof(Derived3) == 1, "Derived3 should be 1 byte"); // No Error
class Derived3  size(1):
   +---
0  | +--- (base class Empty2)
0  | | +--- (base class Empty1)
   | | +---
   | +---
0  | +--- (base class Empty3)
   | +---
0  | c
   +---

All of Derived3‘s subobjects are laid out at offset 0, and its size is the optimal 1 byte. One important point to remember is that __declspec(empty_bases) only affects the layout of the class to which it is applied; it is not applied recursively to base classes:

struct __declspec(empty_bases) Derived5 : Derived4
{
};
static_assert(sizeof(Derived5) == 4, "Derived5 should be 4 bytes"); // Error
class Derived5  size(8):
   +---
0  | +--- (base class Derived4)
0  | | +--- (base class Empty2)
0  | | | +--- (base class Empty1)
   | | | +---
   | | +---
1  | | +--- (base class Empty3)
   | | +---
   | | <alignment member> (size=3)
4  | | i
   | +---
   +---

Although __declspec(empty_bases) is applied to Derived5, it is not eligible for EBCO because it doesn’t have any direct empty base classes, so it has no effect. However, if it is instead applied to the Derived4 base class, which is eligible for EBCO, both Derived4 and Derived5 will have optimal layout:

struct __declspec(empty_bases) Derived4 : Empty2, Empty3
{
  int i;
};
static_assert(sizeof(Derived4) == 4, "Derived4 should be 4 bytes"); // No Error
struct Derived5 : Derived4
{
};
static_assert(sizeof(Derived5) == 4, "Derived5 should be 4 bytes"); // No Error
class Derived5  size(4):
   +---
0  | +--- (base class Derived4)
0  | | +--- (base class Empty2)
0  | | | +--- (base class Empty1)
   | | | +---
   | | +---
0  | | +--- (base class Empty3)
   | | +---
0  | | i
   | +---
   +---

To determine which classes would benefit from __declspec(empty_bases), a new “undocumented” /d1reportClassLayoutChanges compiler option has been added that reports the default layout as well as the EBCO layout for any class that would directly benefit from its use. It is recommended to compile only a single file at a time with this option so as to avoid superfluous output. Furthermore, this option is unsupported and intended only for informational purposes and should not be used for regular project builds.

Accessing the compiler options for a single file

Adding /d1reportClassLayoutChanges as an additional option

The class layout information will be included in the project’s build log, which is generated in the project’s Intermediate directory.

Compiling the original examples with /d1reportClassLayoutChanges would output:

Effective Layout: (Default)
class Derived3  size(2):
   +---
0  | +--- (base class Empty2)
0  | | +--- (base class Empty1)
   | | +---
   | +---
1  | +--- (base class Empty3)
   | +---
1  | c
   +---
Future Default Layout: (Empty Base Class Optimization)
class Derived3  size(1):
   +---
0  | +--- (base class Empty2)
0  | | +--- (base class Empty1)
   | | +---
   | +---
0  | +--- (base class Empty3)
   | +---
0  | c
   +---
Effective Layout: (Default)
class Derived4  size(8):
   +---
0  | +--- (base class Empty2)
0  | | +--- (base class Empty1)
   | | +---
   | +---
1  | +--- (base class Empty3)
   | +---
   | <alignment member> (size=3)
4  | i
   +---
Future Default Layout: (Empty Base Class Optimization)
class Derived4  size(4):
   +---
0  | +--- (base class Empty2)
0  | | +--- (base class Empty1)
   | | +---
   | +---
0  | +--- (base class Empty3)
   | +---
0  | i
   +---

This shows that the effective layout for Derived3 and Derived4 is the default layout and that the EBCO layout would cut their sizes in half. After applying __declspec(empty_bases) to a class, the output will indicate that its effective layout is the EBCO layout. Because it is possible for a class to be non-empty with the default layout but be empty with the EBCO layout, you may need to iterate compiling with /d1reportClassLayoutChanges and applying __declspec(empty_bases) until the entire class hierarchy makes complete use of the EBCO layout.

Due to the aforementioned requirement that all object files and libraries agree on the class layout, __declspec(empty_bases) can only be applied to classes that you control. It cannot be applied to classes in the STL nor those that are included in libraries that are not also re-compiled with the EBCO layout.

When the default layout is changed in a future major release of the Visual C++ Compiler Toolset, __declspec(empty_bases) will no longer have any effect, as every class will make complete use of EBCO. However, in scenarios involving interop with other languages or dependencies with DLLs that cannot be recompiled, it may be the case that you do not want the layout of a particular class to change when the default is changed. To address such scenarios, a __declspec(layout_version(19)) attribute has also been added, which will result in the class layout being identical to the layout in Visual Studio 2015, even after the default layout changes. This attribute has no effect on code compiled with Visual Studio 2015, but can be applied proactively to inhibit future default class layout changes.

One known issue with the current behavior of __declspec(empty_bases) is that it may violate a standard requirement that two subobjects that have the same class type and that belong to the same most derived object are not allocated at the same address:

struct __declspec(empty_bases) Derived6 : Empty1, Empty2
{
  char c;
};
class Derived6 size(1):
   +---
0  | +--- (base class Empty1)
   | +---
0  | +--- (base class Empty2)
0  | | +--- (base class Empty1)
   | | +---
   | +---
0  | c
   +---

Derived6 contains two subobjects of type Empty1, as there is no virtual inheritance, but they are both laid out at offset 0, which is in violation of the standard. This issue will be fixed in Visual Studio 2015 Update 3; however, doing so will result in such classes having different EBCO layouts in Update 2 and Update 3. Classes that use the default layout won’t be affected by this change. Therefore, __declspec(empty_bases) should not be applied to such classes until Update 3, and should only be applied if compatibility with the Update 2 EBCO layout will not be required. We hope your code can benefit from this improvement to our EBCO support and we look forward to your feedback.

Vinny Romano Visual C++ Team

Posted in C++

0 comments

Discussion is closed.

Feedback usabilla icon