美文网首页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++内存对齐及内存布局

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