引入
举个简单的例子:(环境:64位Windows 11,GCC,C++20,O3优化)
#include <iostream>
struct A1 {
char a;
int b;
char c;
};
struct A2 {
char a;
char b;
int c;
};
int main() {
std::cout << sizeof(A1) << ' ' << sizeof(A2) << '\n'; // 12 8
return 0;
}
为什么两个结构体明明有同样的内容,却有不同大小的内存呢?因为内存对齐!
内存对齐规则
1、没有#pragma pack宏的对齐规则
- 结构体的起始存储位置必须是能够被该结构体中最大的数据类型所整除。
- 每个数据成员存储的起始位置是自身大小的整数倍(比如int在64位机为4字节,则int型成员要从4的整数倍地址开始存储)。
- 结构体总大小(也就是sizeof的结果),必须是该结构体成员中最大的对齐模数的整数倍。若不满足,会根据需要自动填充空缺的字节。
- 结构体包含另一个结构体成员,则被包含的结构体成员要从其原始结构体内部最大对齐模数的整数倍地址开始存储。(比如struct a里存有struct b,b里有char,int,double等元素,那b应该从8的整数倍开始存储。)
- 结构体包含数组成员,比如char a[3],它的对齐方式和分别写3个char是一样的,也就是说它还是按一个字节对齐。如果写:typedef char Array[3],Array这种类型的对齐方式还是按一个字节对齐,而不是按它的长度3对齐。
- 结构体包含共用体成员,则该共用体成员要从其原始共用体内部最大对齐模数的整数倍地址开始存储。
2、存在#pragma pack宏的对齐
#pragma pack (n) //编译器将按照n个字节对齐
#pragma pack () //取消自定义字节对齐方式
那么对齐规则就变成下面的
- 结构,联合,或者类的数据成员,第一个放在偏移为0的地方,以后每个数据成员的对齐,按照#pragma pack指定的数值和自身对齐模数中较小的那个。
回到上面的程序,按照上述规则,两个结构体都按照最大的数据类型int的 4 字节对齐。第一个结构体,char占1字节,由于接下来是int类型的四个字节,所以会填充为4字节,int过后又有一个char,再次被填充4字节,总共 4 + 4 + 4 = 12 字节。第二个结构体,两个char占2字节,后跟一个int,于是被填充为4字节,总共 4 + 4 = 8 字节。
这很对,但这东西到底有什么用?
内存对齐的作用
内存对齐在实际开发中有很多用途,以下是其中一些:
- 提高程序效率:内存对齐可以使变量存储在自然边界上,从而提高 CPU 访问内存的效率,从而提高程序的效率。
- 保证内存安全:当结构体或类中的成员变量按照对齐规则进行分配时,可以确保数据不会因为字节对齐而出现错误的偏移量,从而保证内存的安全性。
- 节省内存空间:通过内存对齐,可以使变量的存储更加紧凑,减少内存碎片,从而节省内存空间。
- 优化缓存行访问:现代 CPU 采用了缓存行的概念,内存对齐可以让变量存储在相邻的缓存行中,从而提高 CPU 访问缓存的效率。
- 改善并行处理效率:内存对齐可以改善多线程并发处理时的效率,因为当多个线程访问同一内存时,内存对齐可以减少线程之间的冲突,从而提高并行处理效率。
- 确保兼容性:不同编译器或操作系统可能有不同的内存对齐规则,因此在开发中遵循正确的内存对齐规则可以确保程序的兼容性和可移植性。
自定义网络通讯协议包通常都是定义成struct的形式, 而struct会自动内存对齐,这会造成结构体成员间有”空洞“,传给其它平台后,其它平台弄不清楚原平台是按什么方式对齐的,只会按自己的方式解包。 解出来的结果有可能是错误的。为防止这种情况出现,自定义通讯协议时,一定要让结构体(或联合)成员间都按1来对齐。
总之,内存对齐是一种重要的技术,它在实际开发中可以优化程序性能、提高程序安全性、节省内存空间等,因此开发人员需要深入理解内存对齐的原理和规则,并根据实际情况进行合理的内存对齐
内存对齐与位域
位域是一种控制内存二进制位的技术
struct A {
uint16_t a : 1;
uint16_t b : 5;
uint16_t c : 3;
};
// sizeof(A) == 2;
如上便可指定a占用1bit(bit不是byte!),b 5bit,c 3 bit
位域的对齐规则如下:
当相邻位域成员的类型相同时,如果它们的位宽之和小于类型大小,那么后面的成员紧邻前一个成员存储;如果它们的位宽之和大于类型大小,那么后面的成员将存在下一个类型大小的空间
如果相邻的位域的数据类型不相同,则不同编译器实现不一样,有些编译器选择不压缩
如果位域不连续,中间含非位域,则按标准数据类型大小划分,比如
struct A { uint16_t a : 2; int b; uint16_t c : 3; }; // sizeof(A) == 12;
对于第一个位域对齐规则再举个例子
#include <iostream>
#include <bitset>
struct A {
uint16_t a : 1;
uint16_t b : 5;
uint16_t c : 3;
};
union {
A a;
uint16_t b;
} op;
int main() {
op.a.a = 1;
op.a.b = 1;
op.a.c = 1;
// 输出 b 的二进制表示
std::cout << std::bitset<16>(op.b) << '\n';
// 0000000001000011
op.a.a = 0;
std::cout << std::bitset<16>(op.b) << '\n';
// 0000000001000010
op.a.b = 0;
std::cout << std::bitset<16>(op.b) << '\n';
// 0000000001000000
op.a.c = 0;
std::cout << std::bitset<16>(op.b) << '\n';
// 0000000000000000
return 0;
}
C++11 alignas说明符
该说明符可以接受类型或者常量表达式。特别需要注意的是,该常量表达式计算的结果必须是一个2的幂值,否则是无法通过编译的。具体用法如下(这里采用GCC 编译器,因为其alignof可以查看变量的对齐字节长度)
#include <iostream>
struct X {
char a1;
int a2;
double a3;
};
struct X1 {
alignas(16) char a1;
alignas(double) int a2;
double a3;
};
struct alignas(16) X2 {
char a1;
int a2;
double a3;
};
struct alignas(16) X3 {
alignas(8) char a1;
alignas(double) int a2;
double a3;
};
#define COUT_ALIGN(s) \
std::cout << "alignof(" #s ") = " << alignof(s) << std::endl
#define COUT_SIZE(s) \
std::cout << "sizeof(" #s ") = " << sizeof(s) << std::endl
int main() {
X x;
X1 x1;
X2 x2;
X3 x3;
alignas(4) X3 x4;
COUT_ALIGN(x); // 8
COUT_ALIGN(x1); // 16
COUT_ALIGN(x2); // 16
COUT_ALIGN(x3); // 16
COUT_ALIGN(x4); // 4
COUT_ALIGN(x4.a1); // 8
COUT_SIZE(x); // 16
COUT_SIZE(x1); // 32
COUT_SIZE(x2); // 16
COUT_SIZE(x3); // 32
COUT_SIZE(x4); // 32
COUT_SIZE(x4.a1); // 1
return 0;
}
在例子中,X的类型对齐字节长度为8字节,而X2在使用了alignas(16)之后,对齐字节长度修改为了16字节。
另外, 如果修改结构体成员的对齐字节长度,那么结构体本身的对齐字节长度也会发生变化,因为结构体类型的对齐字节长度总是需要大于或者等于其成员变量类型的对齐字节长度。
比如X1的成员变量a1类型的对齐字节长度修改为了16字节,所有X1类型也被修改为16字节对齐。
同样的规则也适用于结构体X3,X3类型的对齐字节长度被指定为16字节,虽然其成员变量a1的类型对齐字节长度被指定为8字节,但是并不能改变X3类型的对齐字节长度。
最后要说明的是,结构体类型的对齐字节长度,并不能影响声明变量时变量的对齐字节长度,比如x4。
不过在变量声明时指定对齐字节长度,也不影响变量内部成员变量类型的对齐字节长度,比如x4.a1。上面的代码用结构体作为例子,实际上对于类也是一样的。
参考:
chatGPT