美文网首页C++
C++内存对齐及内存布局

C++内存对齐及内存布局

作者: crossous | 来源:发表于2019-03-26 12:52 被阅读21次

前言

  本文会展示内存对齐,及继承、虚继承等各个情况下内存的布局,并根据结果总结使用场景。

基本调试方法

  使用编译器自带的工具,在Visual Studio下,右键解决方案,在弹出的菜单下,点击属性:

  在属性页中,依次找到 配置属性 》C/C++ 》命令行,在【其他选项】中输入/d1 reportAllClassLayout,点击确定。   随后重新编译(重新生成解决方案),就可以在输出栏看到内存的布局(输出不少,可以点击输出栏后,用CTRL+F查找):   当然,查看单个类也是可以的,只需要输入:/d1 reportSingleClassLayout○○○,用类名直接代替最后的○○○即可。

一、内存对齐

1.单个变量

  C++普通类占用内存的只有成员变量,普通成员函数不占用类的内存空间:

class TEST{
    int a;
};
//class TEST    size(4):
//  +---
// 0    | a
//  +---

2.多个变量

class TEST{
    double d;
    int a;
};
//class TEST    size(16):
//  +---
// 0    | d
// 8    | a
//      | <alignment member> (size=4)
//  +---

  C++默认对齐大小为类内最大的基础类型大小,因此int变量后多出4个字节的对齐空间。

3.多变量实验

class TEST{
    char c;
    int a;
    double d;
};
//class TEST    size(16):
//  +---
// 0    | c
//      | <alignment member> (size=3)
// 4    | a
// 8    | d
//  +---
class TEST{
    int a;
    char c;
    double d;
};
//class TEST    size(16):
//  +---
// 0    | a
// 4    | c
//      | <alignment member> (size=3)
// 8    | d
//  +---
class TEST{
    double d;
    int a;
    char c;
};
//class TEST    size(16):
//  +---
// 0    | d
// 8    | a
//12    | c
//      | <alignment member> (size=3)
//  +---

  这三个都没有变化,但假如:

class TEST{
    char c;
    double d;
    int a;
};
//class TEST    size(24):
//  +---
// 0    | c
//      | <alignment member> (size=7)
// 8    | d
//16    | a
//      | <alignment member> (size=4)
//  +---

4.内存对齐规则

  发生了变化,多了8个字节,由此得到规律:
  想像一个表格,列数为拥有最大基础类型长度,例如上面的double,长度为8字节,则每行8列:

对齐\字节 1 2 3 4 5 6 7 8
0
8
16

  将变量从上到下填充,塞入当前变量时,如果能在当前行塞下,就塞入,塞不下,就另起一行再塞入:

对齐\字节 1 2 3 4 5 6 7 8
0 char
8 double_1 double_2 double_3 double_4 double_5 double_6 double_7 double_8
16 int_1 int_2 int_3 int_4

  同时,每个变量只会放在整[自己大小的]的字节处,double只会从整8字节开始,int只会从整4字节开始,short只会从整2字节开始,char就随意放置,上面多变量实验第一例的内存布局就如下图所示:

对齐\字节 1 2 3 4 5 6 7 8
0 char int_1 int_2 int_3 int_4
8 double_1 double_2 double_3 double_4 double_5 double_6 double_7 double_8

数组成员

  数组成员相当于在当前位置直接定义当前数组长度个变量,对齐长度不会变成数组占用的字节数。

非基础类型成员

struct Box {
    double a;
    char b;
};
class TEST{
    Box b;
    char c;
};
//class Box size(16):
//  +---
// 0    | a
// 8    | b
//      | <alignment member> (size=7)
//  +---
//
//class TEST    size(40):
//  +---
// 0    | Box b
//16    | c
//      | <alignment member> (size=7)
//  +---

  相当于直接把成员对象空间堆在里面,同时持有类的对齐也会受到成员对象的影响,上例TEST的对齐大小受Box的影响,变成了8

5.更大的对齐

  看起来内存对齐的最大对齐就是8,毕竟我们所熟知的类型中,只有double才会占用8字节,非基础类型成员的大小也不会直接作用于内存对齐。
  不过更大的基础类型也是存在的,就比如在SIMD类型中_m128和_m256分别会占用16字节和32字节,并且它们的空间占用会导致内存对齐数的增大。如果你的类中只有_m256和char的变量各一个,用上面的方法,会看到char后面会跟着31个内存对齐空间。
  这两个类型分别在nmmintrin.himmintrin.h中,相关操作可以查阅SIMD的资料。

6.结论

  不同的变量排列方式会改变对象的大小,建议变量从大到小或从小到大排列,这样空间会达到最优。
  学会内存的对齐有什么意义呢?从上例来看,好像只能优化对象的布局,使其占用空间更小。在当今的计算机,我们普遍不关注几个字节的空间占用,一个用途,就是如果数据使用结构化存储,当数据量十分庞大,成千上万,乃至千万、上亿时,省下的空间能肉眼可见;不过这种存储技术通常都有相关优化,一般也不是我们所关心的。
  不过我在编写代码时,遇到了一定要内存对齐的情况,是编写SIMD(单指令流多数据流)时遇到的,详情可见SIMD类型堆上分配方法探究

7.其他有关内存布局的C++特性与函数

①_declspec关键字
  __declspec用于指定所给定类型的实例的与Microsoft相关的存储方式,通常用法是__declspec(表达式),当表达式为align(n)时(n为2的整次幂),可设置对象地址对齐:

_declspec(align(16)) class TEST {
    char c;
};
//class TEST    size(16):
//  +---
// 0    | c
//  +---

  可以看到对象最小大小为16,不过最终大小可以不为16的倍数,对齐地址和对齐内存块还是有区别的,这种方式能对齐静态存储,但动态分配的内存不能保证对齐地址。
②alignas、alignof关键字
  C++11标准的关键字。
  alignof可在运行时得到类型的对齐值:

_declspec(align(4)) class TEST {
    double a;
};
int main() {
    std::cout << alignof(TEST);//output: 8
_declspec(align(16)) class TEST {
    double a;
};//output: 16

  alignas可在定义变量时,改变当前变量的对齐值:

_declspec(align(16)) class TEST {
    double a;
    double b;
    alignas(32) char c;
};
int main() {
    std::cout << alignof(TEST);//output: 32
    system("pause");
}
//class TEST    size(64):
//  +---
// 0    | a
// 8    | b
//      | <alignment member> (size=16)
//32    | c
//      | <alignment member> (size=31)
//  +---

③_mm_malloc, _mm_free函数
  SIMD库带的函数,用于分配地址对齐的动态内存,常常和placement new配合使用,使用事例参照上面那个遇到的SIMD坑。

二、继承、带有虚函数、虚继承的内存布局

1.单个类

  带有一个或多个虚函数,将得到一个虚表指针vfptr:

class TEST{
    int a;
    virtual bool func() {}
};
//class TEST    size(8):
//  +---
// 0    | {vfptr}
// 4    | a
//  +---

  虚表指针vfptr会指向存有虚函数的虚表,虚表本身大小我们不必考虑。
  vfptr会放到成员变量前,单单看这个例子,vfptr的大小是4,不过如果增加一个更大的变量:

class TEST{
    double d;
    int a;
    virtual bool func() {}
};
//class TEST    size(24):
//  +---
// 0    | {vfptr}
// 8    | d
//16    | a
//      | <alignment member> (size=4)
//  +---

  在x64平台上vfptr大小变成了8,在x86平台上,大小还是4,但内存会对齐四个,最终占用大小还是8;如果是SIMD类型,vfptr的大小甚至可能是16、32。

2.继承

  此时出现继承,有四种情况,有无虚函数、有无虚继承的两两组合:

  • 无虚继承,无虚函数
class TEST2 :  public TEST {
    int a2;
};
//class TEST2   size(32):
//  +---
// 0    | +--- (base class TEST)
// 0    | | {vfptr}
// 8    | | d
//16    | | a
//  | | <alignment member> (size=4)
//  | +---
//24    | a2
//      | <alignment member> (size=4)
//  +---

  直接把父类的内存放到自己的最前面,同时内存布局继承父类的(父类有double,内存对齐为8,子类也对齐为8),和上面的对象成员方式基本一致。

  • 无虚继承,有虚函数
class TEST2 :  public TEST {
    int a2;
    virtual void func2() {}
};

  生成结果和无虚继承,无虚函数等同,没有变化(没出现虚表指针vfptr),推测为:和父类共用虚表指针。

  • 虚继承,无虚函数
class TEST2 :  virtual public TEST {
    int a2;
};
//class TEST2   size(32):
//  +---
// 0    | {vbptr}
// 4    | a2
//  +---
//  +--- (virtual base TEST)
// 8    | {vfptr}
//16    | d
//24    | a
//      | <alignment member> (size=4)
//  +---

  在自身成员变量前,增加指向父类的虚指针(vbptr),这个虚指针的大小和对齐不与父类相同,但如果自身用更大的基础变量,同样会导致vbptr向更大变量对齐:

class TEST2 :  virtual public TEST {
    int a2;
    double d2;
};
//class TEST2   size(48):
//  +---
// 0    | {vbptr}
// 8    | a2
//      | <alignment member> (size=4)
//16    | d2
//  +---
//  +--- (virtual base TEST)
//24    | {vfptr}
//32    | d
//40    | a
//      | <alignment member> (size=4)
//  +---

  图中可见,TEST2的vbptr的大小变成了8。

  • 虚继承,有虚函数
class TEST2 :  virtual public TEST {
    int a2;
    double d2;
    virtual void func2() {}
};
//class TEST2   size(56):
//  +---
// 0    | {vfptr}
// 8    | {vbptr}
//16    | a2
//      | <alignment member> (size=4)
//24    | d2
//  +---
//  +--- (virtual base TEST)
//32    | {vfptr}
//40    | d
//48    | a
//      | <alignment member> (size=4)
//  +---

  在父类虚指针vbptr前,再次出现了自己指向虚表的虚指针vfptr,其大小变化方式和vbptr相同,在这个有double类型的TEST2中,大小都变成了8。

3.总结

三、钻石继承结构的内存布局

1.一把梭式的直接继承

class TEST{
    int a;
    virtual bool func() {}
};

class TEST2 :  public TEST {
    int a2;
    virtual void func2() {}
};

class TEST3 : public TEST {
    int a3;
    virtual void func3() {}
};

class TEST4 : public TEST2, public TEST3 {
    int a4;
    virtual void func4() {}
};
//class TEST    size(8):
//  +---
// 0    | {vfptr}
// 4    | a
//  +---
//class TEST2   size(12):
//  +---
// 0    | +--- (base class TEST)
// 0    | | {vfptr}
// 4    | | a
//  | +---
// 8    | a2
//  +---
//TEST3与TEST2一致
//.....
//class TEST4   size(28):
//  +---
// 0    | +--- (base class TEST2)
// 0    | | +--- (base class TEST)
// 0    | | | {vfptr}
// 4    | | | a
//  | | +---
// 8    | | a2
//  | +---
//12    | +--- (base class TEST3)
//12    | | +--- (base class TEST)
//12    | | | {vfptr}
//16    | | | a
//  | | +---
//20    | | a3
//  | +---
//24    | a4
//  +---

  和前面说的一样,都是直接堆放,可以发现,TEST2和TEST3的空间中,各有一个TEST。

2.各层虚继承

  我们将上图从上到下分为1,2,3三层。

①2对1层单个虚继承实验
class TEST2 : virtual public TEST {
    int a2;
    virtual void func2() {}
};
//其他不变
//class TEST2   size(20):
//  +---
// 0    | {vfptr}
// 4    | {vbptr}
// 8    | a2
//  +---
//  +--- (virtual base TEST)
//12    | {vfptr}
//16    | a
//  +---
//class TEST4   size(36):
//  +---
// 0    | +--- (base class TEST2)
// 0    | | {vfptr}
// 4    | | {vbptr}
// 8    | | a2
//  | +---
//12    | +--- (base class TEST3)
//12    | | +--- (base class TEST)
//12    | | | {vfptr}
//16    | | | a
//  | | +---
//20    | | a3
//  | +---
//24    | a4
//  +---
//  +--- (virtual base TEST)
//28    | {vfptr}
//32    | a
//  +---

  TEST2变成了前面虚继承,有虚函数的情况,而TEST4因为未采取虚继承,依旧是直接把TEST2直接放在前面

②2对1层双虚继承实验
//对TEST3做与TEST2相同的变化,既变为虚继承
//class TEST4   size(36):
//  +---
// 0    | +--- (base class TEST2)
// 0    | | {vfptr}
// 4    | | {vbptr}
// 8    | | a2
//  | +---
//12    | +--- (base class TEST3)
//12    | | {vfptr}
//16    | | {vbptr}
//20    | | a3
//  | +---
//24    | a4
//  +---
//  +--- (virtual base TEST)
//28    | {vfptr}
//32    | a
//  +---

  TEST4的粗暴堆放没有变化,但TEST只有一个了!

③2对1层全虚继承,3对2层单虚继承
class TEST4 : virtual public TEST2, public TEST3 {
    int a4;
    virtual void func4() {}
};
//class TEST4   size(36):
//  +---
// 0    | +--- (base class TEST3)
// 0    | | {vfptr}
// 4    | | {vbptr}
// 8    | | a3
//  | +---
//12    | a4
//  +---
//  +--- (virtual base TEST)
//16    | {vfptr}
//20    | a
//  +---
//  +--- (virtual base TEST2)
//24    | {vfptr}
//28    | {vbptr}
//32    | a2
//  +---

  TEST4只堆放TEST3,TEST2不再被堆放,但TEST4的父节点指针vbptr和虚表指针vfptr都未出现。

④全虚继承
//class TEST4   size(44):
//  +---
// 0    | {vfptr}
// 4    | {vbptr}
// 8    | a4
//  +---
//  +--- (virtual base TEST)
//12    | {vfptr}
//16    | a
//  +---
//  +--- (virtual base TEST2)
//20    | {vfptr}
//24    | {vbptr}
//28    | a2
//  +---
//  +--- (virtual base TEST3)
//32    | {vfptr}
//36    | {vbptr}
//40    | a3
//  +---

  TEST4的vfptr和vbptr同时出现了!

⑤3对2层全虚继承,2对1层非全虚继承
//class TEST4   size(44):
//  +---
// 0    | {vfptr}
// 4    | {vbptr}
// 8    | a4
//  +---
//  +--- (virtual base TEST2)
//12    | +--- (base class TEST)
//12    | | {vfptr}
//16    | | a
//  | +---
//20    | a2
//  +---
//  +--- (virtual base TEST)
//24    | {vfptr}
//28    | a
//  +---
//  +--- (virtual base TEST3)
//32    | {vfptr}
//36    | {vbptr}
//40    | a3
//  +---

  TEST2再次堆放TEST空间,并且TEST3也指向一个TEST空间,可以看出2对1层未全虚继承,就无法消除存在多个TEST的歧义。

⑥更多继承
  如果有后续的继承结构,例如下面:
class TEST4 : virtual public TEST2, public TEST3 {
    int a4;
    virtual void func4() {}
};

class TEST5 : public TEST3 {
    int a5;
    virtual void func5() {}
};

class TEST6 : virtual public TEST4, virtual public TEST5 {
    int a6;
    virtual void func6() {}
};
//class TEST6   size(64):
//  +---
// 0    | {vfptr}
// 4    | {vbptr}
// 8    | a6
//  +---
//  +--- (virtual base TEST)
//12    | {vfptr}
//16    | a
//  +---
//  +--- (virtual base TEST2)
//20    | {vfptr}
//24    | {vbptr}
//28    | a2
//  +---
//  +--- (virtual base TEST4)
//32    | +--- (base class TEST3)
//32    | | {vfptr}
//36    | | {vbptr}
//40    | | a3
//  | +---
//44    | a4
//  +---
//  +--- (virtual base TEST5)
//48    | +--- (base class TEST3)
//48    | | {vfptr}
//52    | | {vbptr}
//56    | | a3
//  | +---
//60    | a5
//  +---

  如果TEST4和TEST5未保证对TEST3的虚继承,TEST6就会存在两个TEST3,不过TEST依旧只存在一个。
  不过这种情况下,TEST4对TEST2是否为虚继承就无关紧要了

3.结论

  2对1层的双虚继承,就足够保证消除二义性,如果保证TEST4这一层未来不会继续被继承,可以不用保证3对2层的双虚继承,这样能省下两个虚指针的空间;如果有后续的继承,那么要根据需要(是否要消除歧义、继承图的样子等),来选择是否要虚继承。

相关文章

  • C++内存对齐及内存布局

    前言   本文会展示内存对齐,及继承、虚继承等各个情况下内存的布局,并根据结果总结使用场景。 基本调试方法   使...

  • golang 和 C++ 的内存对齐

    golang 和 C++的内存对齐,基本一致,记住规则和对应类型的 size 即可 内存对齐规则 有效对齐值是固定...

  • sse中内存对齐问题

    c++ sse中无论声明栈内存还是堆内存都需要声明内存对齐,在VC++中:堆内存分配使用_aligned_mall...

  • sizeof与字节对齐

    参考 【面试题】sizeof引发的血案编译器与字节对齐c 语言字节对齐问题详解C/C++内存对齐内存存取粒度C和C...

  • 2.iOS底层学习之内存对齐

    学习了内存对齐之后的疑问?? 1.为啥要内存对齐?2.内存对齐的规则?3.内存对齐实例分析。 内存对齐的目的 上网...

  • 内存对齐

    本次主要讨论三个问题: 什么是内存对齐 内存对齐的好处 如何对齐 内存对齐 内存对齐是一种提高内存访问速度的策略。...

  • 结构体内存对齐

    对象内存对齐 探讨的问题 1.什么是内存对齐?2.为什么要做内存对齐?3.结构体内存对齐规则4.源码内存对齐算法 ...

  • NSObject 底层本质

    一、OC 转 C/C++ 二、NSObject 对象内存布局 三、NSObject 内存大小 四、OC 对象内存布...

  • C++之内存布局

    在C++之内存管理一文中,我们已经了解到C++的内存管理,这里介绍C++的典型内存布局结构。 1、总体来说,C/C...

  • c++内存对齐

    1、为什么要进行内存对齐呢? 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台...

网友评论

    本文标题:C++内存对齐及内存布局

    本文链接:https://www.haomeiwen.com/subject/oemwvqtx.html