如何确定C++类的大小?

Gab*_*ern 10 c++ memory-alignment

简介:编译期间编译器如何静态地确定C++类的大小?

细节:

我试图了解规则用于确定类将使用多少内存,以及内存如何对齐.

例如,以下代码声明了4个类.前2个是16个字节.但是3是48个字节,即使它包含与前2个相同的数据成员.而第四个类具有与第三个相同的数据成员,只是以不同的顺序,但它是32个字节.

#include <xmmintrin.h>
#include <stdio.h>

class TestClass1 {
  __m128i vect;
};

class TestClass2 {
  char buf[8];
  char buf2[8];
};

class TestClass3 {
  char buf[8];
  __m128i vect;
  char buf2[8];
};

class TestClass4 {
  char buf[8];
  char buf2[8];
  __m128i vect;
};


TestClass1 *ptr1;
TestClass2 *ptr2;
TestClass3 *ptr3;
TestClass4 *ptr4;
int main() {
  ptr1 = new TestClass1();
  ptr2 = new TestClass2();
  ptr3 = new TestClass3();
  ptr4 = new TestClass4();
  printf("sizeof TestClass1 is: %lu\t TestClass2 is: %lu\t TestClass3 is: %lu\t TestClass4 is: %lu\n", sizeof(*ptr1), sizeof(*ptr2), sizeof(*ptr3), sizeof(*ptr4));
  return 0;
}
Run Code Online (Sandbox Code Playgroud)

我知道答案与班级数据成员的对齐有关.但我试图准确理解这些规则是什么以及如何在编译步骤中应用它们,因为我有一个具有__m128i数据成员的类,但数据成员不是16字节对齐的,这会导致编译时出现段错误使用movaps访问数据生成代码.

Eri*_*hil 13

对于POD(普通旧数据),规则通常是:

  • 结构中的每个成员都有一些大小s和一些对齐要求a.
  • 编译器以大小S设置为零并且对齐要求A设置为1(字节)开始.
  • 编译器按顺序处理结构中的每个成员:
    1. 考虑成员的对齐要求a.如果S当前不是a的倍数,则添加足够的字节S,使其为a的倍数.这决定了会员的去向; 它将从结构的开头到达偏移量S(对于S的当前值).
    2. 将A设置为A和a的最小公倍数.
    3. 添加s到S,为成员留出空间.
  • 当为每个成员完成上述过程时,请考虑结构的对齐要求A.如果S当前不是A的倍数,则向S添加足够的S以使其为A的倍数.

当完成上述操作时,结构的大小是S的值.

另外:

  • 如果任何成员是数组,则其大小是元素的数量乘以每个元素的大小,其对齐要求是元素的对齐要求.
  • 如果任何成员是结构,则其大小和对齐要求如上计算.
  • 如果任何成员是工会:
    1. 将S设置为最大成员的大小.
    2. 将A设置为所有成员的最小公倍数.
    3. 如果S不是A的倍数,则添加足够的S以使其成为A的倍数.

考虑你的TestClass3:

  • S从0开始,A从1开始.
  • char buf[8] 需要8个字节和对齐1,因此S增加8到8,A保持为1.
  • __m128i vect需要16个字节和对齐16.首先,S必须增加到16才能给出正确的对齐.然后A必须增加到16.然后S必须增加16以腾出空间vect,所以S现在是32.
  • char buf2[8] 需要8个字节和对齐1,因此S增加8到24,A仍然是16.
  • 最后,S是24,它不是A(16)的倍数,因此S必须增加8到32.

所以大小TestClass3是32个字节.

对于基本类型(int,double,等等),所述对准的要求是实施相关的,并且在很大程度上是由硬件来确定.在许多处理器上,当数据具有特定的对齐时(通常当它在内存中的地址是其大小的倍数时),加载和存储数据会更快.除此之外,上述规则主要来自逻辑; 他们将每个成员放在必须满足对齐要求的地方,而不使用超出必要的空间.


Jos*_*eld 8

编译器如何确定类的大小完全取决于编译器.编译器通常会编译以匹配某个应用程序二进制接口,该接口与平台有关.

但是,您观察到的行为非常典型.编译器正在尝试对齐成员,以便它们各自以其大小的倍数开始.在这种情况下TestClass3,其中一个成员是类型__m128isizeof(__m128i) == 16.所以它会尝试将该成员对齐,以16的倍数开始.第一个成员是类型,char[8]因此占用8个字节.如果编译器将_m128i对象直接放在第一个成员之后,它将从位置8开始,这不是16的倍数:

0               8               16              24              32              48
????????????????????????????????????????????????????????????????????????????????????
?    char[8]    ?            __m128i            ?    char[8]    ?           
????????????????????????????????????????????????????????????????????????????????????
Run Code Online (Sandbox Code Playgroud)

所以相反它更喜欢这样做:

0               8               16              24              32              48
????????????????????????????????????????????????????????????????????????????????????
?    char[8]    ?               ?           __m128i             ?    char[8]    ?
????????????????????????????????????????????????????????????????????????????????????
Run Code Online (Sandbox Code Playgroud)

这给它一个48字节的大小.

当您重新排序成员以使TestClass4布局变为:

0               8               16              24              32              48
????????????????????????????????????????????????????????????????????????????????????
?    char[8]    ?    char[8]    ?           __m128i             ?        
????????????????????????????????????????????????????????????????????????????????????
Run Code Online (Sandbox Code Playgroud)

现在一切都正确对齐 - 数组的偏移量是1的倍数(元素的大小),__m128i对象的偏移量是16的倍数 - 总大小是32个字节.

编译器本身不进行此重新排列的原因是因为标准指定类的后续成员应具有更高的地址:

分配具有相同访问控制(第11条)的(非联合)类的非静态数据成员,以便后面的成员在类对象中具有更高的地址.