10

摘要:编译器如何在编译过程中静态确定 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;
}

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

4

4 回答 4

14

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

  • 结构中的每个成员都有一些大小s和一些对齐要求a
  • 编译器从大小S设置为零和对齐要求A设置为一(字节)开始。
  • 编译器按顺序处理结构中的每个成员:
  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 的幂,因此任何一组对齐要求的最小公倍数是其中最大的。

于 2013-01-24T21:14:35.090 回答
8

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

但是,您观察到的行为非常典型。编译器正在尝试对齐成员,以便它们每个都以它们的大小的倍数开始。在 的情况下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 条)的(非联合)类的非静态数据成员,以便后面的成员在类对象中具有更高的地址。

于 2013-01-24T21:23:12.590 回答
0

这些规则是由正在使用的应用程序二进制接口规范确定的,它确保了不同系统之间共享此接口的程序的兼容性。

对于 GCC,这是 Itanium ABI。

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

于 2013-01-24T21:21:27.147 回答
-1

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

于 2013-01-24T21:15:50.837 回答