VS2015 Update2 对空基类的优化

[原文发表地址] Optimizing the Layout of Empty Base Classes in VS2015 Update 2

[原文发表时间] 2016/3/30

C++标准对于类在内存中的布局只有一小部分的要求, 其中之一是绝大多数的派生类在存储区拥有非零的大小并且至少占1字节。 这个需求只适应于派生类, 并不适合基类对象。 基于标准给出的这条规则, 通常会对空基类进行优化(Empty Base Class Optimization EBCO), 这样做节省了内存的开销,提高性能. 过去的VC编译器限制了对空基类的优化. 在新的VS2015 UP2中, 我们为类添加了一个新的属性标识符__declspec(empty_bases), 通过这个标识符得到完全的空基类优化。

    在VS2015 RTM中, 除非指定了__declspec(align())或alignas()标识, 否则空基类的大小总是1字节的长度。

struct Empty1 {};

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

一个非静态数据成员char类型的类大小也是1字节:

struct Struct1

{

       char c;

};

static_assert(sizeof(Struct1) == 1, "Struct1 should be 1 byte");

将类合并为类继承后, 新的派生类的大小也是1字节:

struct Derived1 : Empty1

{

       char c;

};

static_assert(sizeof(Derived1) == 1, "Derived1 should be 1 byte");

这是因为进行了空基类的优化, 如果没有空基类优化, Derived1类的大小应该是2字节, 1字节是父类empty1, 另一字节是Derived1::c。 如果有一系列的空基类, 这时派生类的布局同样也可以优化:

struct Empty2 : Empty1 {};

struct Derived2 : Empty2

{

       char c;

};

static_assert(sizeof(Derived2) == 1, "Derived2 should be 1 byte");

然而, 在多继承的案例中,VS2015 RTM并不会享受到空基类的优化:

struct Empty3 {};

struct Derived3 : Empty2, Empty3

{

       char c;

};

static_assert(sizeof(Derived3) == 1, "Derived3 should be 1 byte"); // Error

虽然Derived3应该是1字节大小,但是默认类的布局导致它变成了2字节大小。类布局的逻辑方法是增加了1字节的偏移值, 这个偏移值来自两个连续的空基类, 导致Empty2消耗了Derived3类中的额外一个字节。

 

class Derived3  size(2) :

+-- -

0 | +-- - (base class Empty2)

0 | | +-- - (base class Empty1)

| | +-- -

| +-- -

1 | +-- - (base class Empty3)

| +-- -

1 | c

+ -- -

导致这种不理想的类布局是由于在合成派生类的过程中需要考虑所有基类(也包含其中的自对象)的对齐,而产生所需的额外偏移值:

struct Derived4 : Empty2, Empty3

{

       int i;

};

static_assert(sizeof(Derived4) == 4, "Derived4 should be 4 bytes"); // Error

对于int类型, 通常对齐方式是4字节, 因此对其Derived4::i时需要额外3字节的偏移值:

class Derived4 size(8) :

       +-- -

       0 | +-- - (base class Empty2)

       0 | | +-- - (base class Empty1)

       | | +-- -

       | +-- -

       1 | +-- - (base class Empty3)

       | +-- -

       | <alignment member> (size = 3)

       4 | i

       + -- -

VS2015 RTM中, 另一个默认类布局优化的问题是空基类会在其后产生位移。

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)

       | +-- -

       +-- -

虽然Struct2的大小是我们期望的大小, 但是empty1在struct2中产生了1个偏移, 但是struct2的大小并没有匹配偏移。 一个含有struct2的数组, A[0]存放Empty1子类型, 并和A[1]拥有相同的地址,这并不是期待的。如果Empty1在struct2内被存放在偏移量为0时,这个问题就不会发生, 因此这也是造成重叠的原因。

如果默认类布局算法可以修改解决这些限制并完全利用空基类优化,那固然很好。然而,这样的一个改变没有在VS2015 RTM当中更新。更新版本中需要兼容最初VS2015RTM版本中中的OBJ文件和生成的类库。如果默认布局发生EBCO优化, 每个obj文件和包含类定义的类库都需要根据新的布局重新编译。同时延伸到一些包含外部代码的扩展类库也需要相应的程序员编译EBCO,非EBCO两个版本, 这样才能保证使用最新VS更新的用户可以编译成功。

 

虽然我们不能改变默认的类布局, 但我们提供了一个方法在原先类基础上的扩展方式从而改变布局,这就是我们在VS2015 UP2中所做的通过额外的类修饰符__declspec(empty_bases)来达到目的。 以这种修饰符定义的类可以完全使用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

所有Derived3的子对象都没有偏移, 它们的大小都是1字节. 需要注意的重点是__declspec(empty_bases)只影响所修饰的类, 并不会影响修饰类的基类:

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

       | +-- -

       +-- -

虽然__declspec(empty_bases)也适用于Derived5,但是并没有进行EBCO,因为它并没有直接的继承空基类,所以修饰符本身并没有做什么。 然而,如果__declspec(empty_bases)修饰在Derived4基类上时, 这时Derived5, Derived4都会被EBCO优化, 如下:

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

       | +-- -

       +-- -

决定哪种类适用于__declspec(empty_bases)修饰符时, 我们新加入了一个未文档化的编译选项/d1reportClassLayoutChanges,使用它来报告类是否可以通过EBCO进行布局优化. 仅以只编译一个文件单元以避免不必要的输出。此外, 这个选项并不属于通常的工程生成, 他只是用来做这些特定的检查。

 

设置访问编译选项

 

在编译选项中添加/d1reportClassLayoutChanges开关

类的布局信息将会包含到项目的生成日志中,生成日志在项目指定的目录下。

使用/d1reportClassLayoutChanges编译上面的例子将会输出:

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

       + -- -

图中很明显EBCO改变了Derived3和Derived4的布局, 并使它们大小减少了一般.使用了__declspec(empty_bases)后,输出的内容将会指明EBCO对类布局的影响 。由于EBCO可造成默认布局非空的类变为一个空类(大小为1),所以你应当对整个类层次加入__declspec(empty_bases)修饰符,并反复使用/d1reportClassLayoutChanges开关进行编译, 直到所有的类层次都使用了EBCO布局。

由于之前提到, 所有的obj文件和类库都会应用类布局, 所以__declspec(empty_bases)只适应与你所完全控制的类, 它并不适用与STL或者在类库中不适用于EBCO布局的类。

当默认布局随着未来VC编译器工具集 改变时,__declspec(empty_bases)就不会产生影响,那时所有类就会完全使用EBCO. 然而, 与其他语言或者独立的dll交互的方案中, 默认类布局就已经改变,这时你或许不需要改变特定类的布局。如果需要解决这种问题,也可以使用__declspec(layout_version(19))修饰符。 即使默认类布局发生变化,它也会保证与VS2015的类布局一样。 这个新修饰符对在VS2015编译的代码没有影响, 但会抑制未来默认类布局发生改变。

__declspec(empty_bases)修饰符一个已知的问题是继承的子类,如果再同时继承相同的子类时, 派生类会对相同的子类分配同样的内存地址, 这点和标准有悖。

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包含两个类型为EMPTY1的子类, 并且没有虚继承,但是他们的偏移都是0, 这和标准有悖。 这个问题将会在VS2015 UP3中修复。 这将导致这种类在VS UP2和VS UP3中有着不同的EBCO布局。 使用默认布局并不会带来这种影响, 因此,在这种情况应该等到VS UP3时再使用__declspec(empty_bases)修饰符。 我们希望你的代码可以收益于新的EBCO技术, 期待您的反馈。

Vinny Romano

 Visual C++ 团队