内存对齐 - 如何确定C++类的大小
memory alignment - How is the size of a C++ class determined?
摘要:编译器如何在编译过程中静态确定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(字节)。
- 编译器按以下顺序处理结构中的每个成员:
- 考虑杆件的对齐要求 a。如果 S 当前不是 a 的倍数,则向 S 添加足够的字节,使其是 a 的倍数。这决定了成员将去哪里;它将从结构的开头偏移 S(对于 S 的当前值)。
- 将 A 设置为 A 和 a 的最小公倍数1。
- 将 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 字节。
对于基本类型(int
、double
等),对齐要求是实现定义的,通常主要由硬件决定。在许多处理器上,当数据具有一定的对齐方式时(通常是当其在内存中的地址是其大小的倍数时),加载和存储数据会更快。除此之外,上述规则主要遵循逻辑;他们将每个成员放在必须满足对齐要求的位置,而不会使用不必要的空间。
脚注
1 对于一般情况,我将其措辞为使用对齐要求的最小常见倍数。但是,由于对齐要求始终是 2 的幂,因此任何一组对齐要求中最小的公倍数是其中最大的。
如何确定类的大小完全取决于编译器。编译器通常会编译以匹配某个依赖于平台的应用程序二进制接口。
但是,您观察到的行为非常典型。编译器正在尝试对齐成员,以便每个成员都从其大小的倍数开始。在 TestClass3
的情况下,其中一个成员的类型为 __m128i
和 sizeof(__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
- 如何确定我已使用非编码文件到达 EOF?
- 函数何时会在c++中包含stack_Unwind_Resume调用
- 如何在C++中确定文本文件中的元素是字符还是数字
- Python中的for循环与C++有何不同
- 在clang++预处理器中确定gcc工具链版本
- 不确定要在我的main中放入什么才能使我的代码正常工作
- C++setiosflags函数操纵器-未确定的缩进
- 编译器如何在使用SFINAE的函数和标准函数之间确定两者是否可行
- 如何使用url确定网站协议
- 通过构造函数动态确定类实现
- 遍历并行数组以确定C++中的最大数字
- C++如果必须在编译时确定大小,std::array 有什么意义?
- 使用不变量来确定二分搜索中的边界条件
- 无论如何,我可以确定构造函数是否存在吗?
- compare_exchange C++函数如何确定竞争条件?
- 如何使用 do while 循环确定最高值和最低值
- 如何确定哪个标头调用 c++ 中的另一个标头
- 在C++中释放内存期间,迭代器与指针有何不同
- C++ Chrono 确定一天是否是周末?
- 什么是C++中的不确定行为?它与未定义的行为有何不同