优化VS2015 Update 2中空基类的布局

C++标准只对一类在内存中如何布局的要求很小,其中一个是大多数派生对象的大小应该具有非零大小,并且应该占用一个或多个字节的存储。由于此要求仅扩展到大多数派生对象,基类子对象不受此约束。利用标准中的这种自由度通常被称为空基类优化(EBCO),可以减少内存消耗,从而提高性能。Visual C++编译器在历史上对EBCO的支持有限;然而,在 Visual Studio 2015更新2 ,我们添加了一个新的 __declspec(empty_bases) 类类型的属性,以充分利用此优化。

null

在Visual Studio 2015中,除非 __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 和1字节 Derived1::c . 当存在空类链时,类布局也是最佳的:

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

但是,Visual Studio 2015中的默认类布局在多继承方案中没有利用EBCO:

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字节,因此需要在后面添加额外的3字节填充 Empty3 正确对齐 Derived4::i :

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

Visual Studio 2015中默认类布局的另一个问题是,空基类的布局可能会超出类的末尾:

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 在偏移量1内布置 Struct2 但它的大小 Struct2 并没有增加来说明这一点。因此,对于数组 A 属于 Struct2 对象的地址 Empty1 的子对象 A[0] 将与的地址相同 A[1] ,应该不是这样。如果 Empty1 以0为偏移量在 Struct2 ,从而重叠 Struct1 子对象。如果可以修改默认的布局算法来解决这些限制并充分利用EBCO,那就太好了;但是,在Visual Studio 2015的更新版本中不能进行这样的更改。更新版本的要求之一是,使用Visual Studio 2015的初始版本生成的对象文件和库继续与使用将来的更新版本生成的对象文件和库兼容。如果类的默认布局由于EBCO而改变,则需要重新编译包含类定义的每个对象文件和库,以便它们都同意类布局。这还将扩展到从外部资源获得的库,这将要求此类库的开发人员提供独立版本,这些版本可以使用EBCO布局编译,也可以不使用EBCO布局编译,这样他们就可以支持那些没有使用最新更新版本编译的客户。虽然我们不能更改默认布局,但我们可以提供一种方法来更改每个类的布局,这就是我们在VisualStudio2015 Update 2中添加 __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 的子对象以偏移量0进行布局,其大小为最佳的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的条件,因为它没有任何直接的空基类,所以它没有任何效果。但是,如果将其应用于 Derived4 基类,它符合EBCO的条件 Derived4 Derived5 将具有最佳布局:

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布局,这些类将直接受益于它的使用。建议使用此选项一次只编译一个文件,以避免多余的输出。此外,此选项不受支持,仅用于提供信息,不应用于常规项目生成。

图片[1]-优化VS2015 Update 2中空基类的布局-yiteyi-C++库

Accessing the compiler options for a single file

图片[2]-优化VS2015 Update 2中空基类的布局-yiteyi-C++库

Adding /d1reportClassLayoutChanges as an additional option

类布局信息将包含在项目的生成日志中,该日志在项目的中间目录中生成。

使用编译原始示例 /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
   +---

这表明,有效的布局 Derived3 Derived4 是默认布局,EBCO布局会将其大小减半。涂抹后 __declspec(empty_bases) 对于一个类,输出将表明它的有效布局是EBCO布局。因为类在默认布局下可能是非空的,而在EBCO布局下可能是空的,所以您可能需要使用编译进行迭代 /d1reportClassLayoutChanges 以及申请 __declspec(empty_bases) 直到整个类层次结构完全使用EBCO布局。

由于上述要求,所有对象文件和库都同意类布局, __declspec(empty_bases) 只能应用于您控制的类。它不能应用于STL中的类,也不能应用于那些包含在库中的类,这些库也没有使用EBCO布局重新编译。

在VisualC++编译器工具的未来主要版本中更改默认布局时, __declspec(empty_bases) 将不再有任何效果,因为每个类都将完全使用EBCO。但是,在涉及与其他语言互操作或与DLL的依赖项无法重新编译的情况下,可能不希望在更改默认值时更改特定类的布局。为了解决这种情况,一个 __declspec(layout_version(19)) 属性,这将导致类布局与VisualStudio2015中的布局相同,即使在默认布局更改之后也是如此。此属性对使用Visual Studio 2015编译的代码没有影响,但可以主动应用以抑制将来的默认类布局更改。

当前行为的一个已知问题 __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处布局,这违反了标准。此问题将在Visual Studio 2015 Update 3中修复;但是,这样做会导致这些类在update2和update3中具有不同的EBCO布局。使用默认布局的类不会受到此更改的影响。因此, __declspec(empty_bases) 在更新3之前不应应用于此类类,并且仅当不需要与更新2 EBCO布局兼容时才应应用。我们希望您的代码可以从我们的EBCO支持的改进中获益,我们期待您的反馈。

维尼罗曼诺Visual C++团队

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享