内存对齐 - 如何确定C++类的大小

memory alignment - How is the size of a C++ class determined?

本文关键字:C++ 何确定 对齐 内存      更新时间:2023-10-16

摘要:编译器如何在编译过程中静态确定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: %lut TestClass2 is: %lut TestClass3 is: %lut TestClass4 is: %lun", sizeof(*ptr1), sizeof(*ptr2), sizeof(*ptr3), sizeof(*ptr4));
  return 0;
}

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

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

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

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

此外:

  • 如果任何成员是数组,则其大小是元素数乘以每个元素的大小,其对齐要求是元素的对齐要求。
  • 如果任何杆件是结构,则其大小和对齐要求按上述方式计算。
  • 如果任何成员是并集,则其大小是其最大成员的大小加上刚好足以使其成为所有成员对齐的最小公倍数1 的倍数。

考虑您的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 字节。

对于基本类型(intdouble等),对齐要求是实现定义的,通常主要由硬件决定。在许多处理器上,当数据具有一定的对齐方式时(通常是当其在内存中的地址是其大小的倍数时),加载和存储数据会更快。除此之外,上述规则主要遵循逻辑;他们将每个成员放在必须满足对齐要求的位置,而不会使用不必要的空间。

脚注

1 对于一般情况,我将其措辞为使用对齐要求的最小常见倍数。但是,由于对齐要求始终是 2 的幂,因此任何一组对齐要求中最小的公倍数是其中最大的。

如何确定类的大小完全取决于编译器。编译器通常会编译以匹配某个依赖于平台的应用程序二进制接口。

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

0               8               16              24              32              48
┌───────────────┬───────────────────────────────┬───────────────┬┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
│    char[8]    │            __m128i            │    char[8]    │           
└───────────────┴───────────────────────────────┴───────────────┴┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄

因此,它更喜欢这样做:

0               8               16              24              32              48
┌───────────────┬┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┬───────────────────────────────┬───────────────┐┄┄┄
│    char[8]    │               │           __m128i             │    char[8]    │
└───────────────┴┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┴───────────────────────────────┴───────────────┘┄┄┄

这使其大小为 48 字节。

当您对成员重新排序以获取TestClass4时,布局将变为:

0               8               16              24              32              48
┌───────────────┬───────────────┬───────────────────────────────┬┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
│    char[8]    │    char[8]    │           __m128i             │        
└───────────────┴───────────────┴───────────────────────────────┴┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄

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

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

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

这些规则由使用的应用程序二进制接口规范一成不变地设置,该规范可确保共享此接口的程序在不同系统之间的兼容性。

对于GCC,这是Itanium ABI。

(不幸的是,它不再公开可用,尽管我确实找到了一面镜子。

如果你想确保一致性,你应该在你的h文件中使用"pragma pack(1)"看看这篇文章:http://tedlogan.com/techblog2.html