C++ 内存对齐详解

一、内存对齐是什么?

计算机从理论上讲可以对任何类型的变量的访问可以从任何地址开始,但由于某些平台原因、性能原因,需要对这些数据在内存中存放的位置做处理,各种类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。但结构体内类型相同的连续元素将在连续的空间内,像数组一样。

内存对齐是由编译器做处理的,该类型占多大内存空间,偏移量是多少,整体的大小是多少,都是由编译器在编译期间就确定了。

通常编译器的默认对齐方式取决于系统架构和编译器设置,在大多数现代架构上,默认对齐方式是8字节(64位架构)或者4字节(32位架构)。


二、对齐原因

1、平台原因(移植原因)

不是所有的硬件平台都能访问任意地址上的任意数据的。某些硬件平台只能在某些地址处取某些特定类型的数据。

2、性能原因

为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。通过空间换时间。


三、对齐规则

1、结构体的第一个成员永远放在0地址处。例如,0xce71d3f460,最低位肯定是0开始。
2、从第二个成员开始,其开始偏移的位置,需要是对齐数的整数倍。对齐数为结构成员自身大小和 默认对齐数的最小值(每个编译器都有自己的默认对齐数)。

3、结构体的总大小必须是最宽基本类型成员大小的整数倍。如果需要,编译器会在最末一个成员之后加上填充字段。
4、如果嵌套了结构体,嵌套的结构体对齐到自己的最大对齐数的整数倍,结构体的整体大小就是所有最大对齐数的整数倍。


四、编译器默认的对齐方式

1、获取编译器默认的对齐方式

c++中可以通过alignof运算法来获取一个类型的对齐方式。alignof返回的是编译器在分配内存时对这个类型的内存对齐要求(单位是字节)。不过alignof是针对具体类型的,而不是针对编译器默认的对齐方式。

但是可以通过检查一个较大类型的对齐要求,比如一个自定义的结构体,它的大小会决定编译器默认的对齐方式。

struct TestStruct
{
	char a;
	int b;
	double c;
};

qDebug() << alignof(TestStruct) << "bytes";  // 我本机是8
2、修改编译器的对齐方式
#pragma pack(n)  // n = 1, 2, 4, 8, 16 (成员之间最小的对齐字节数)

五、计算偏移量

可以通过自己计算偏移量,解析结构体数据(不定义结构体,但知道成员的顺序及类型)。

1、单个结构体偏移量计算以及总大小
int align(int offset, int alignment)
{
	return (offset + alignment - 1) & ~(alignment - 1);
}

struct Variable
{
	int size;       // 类型大小
	int alignment;  // 对齐要求
	void *address;  // 地址
}

int main()
{
	bool a;
	double b;
	short c;
	char d;

	st::vector<Variable> vs = {
		{sizeof(bool), alignof(bool), &a},
		{sizeof(double), alignof(double), &b},
		{sizeof(short), alignof(short), &c},
		{sizeof(char), alignof(char), &d},
	};

	int offset = 0;
	int memberMax = 0;
	for (const auto& var : vs)
	{
		offset = align(offset, var.alignment);  // 根据上一个变量的实际位置,计算当前变量开始偏移的位置
		// memcpy(var.address, networkData + offset, var.size);
		qDebug() << "start pos" << offset;
		offset += var.size;                     // 计算当前变量偏移后的位置(未填充)
		qDebug() << "end pos" << offset;
		
		if (var.alignment > memberMax)
			memberMax = var.alignment;
	}
	
	int total = align(offset, memberMax);      // 总大小 = 最宽基本类型成员大小的整数倍
	return 0;
}
start posend pos
01
816
1618
1819

total = align(19, 8) = 24 bytes


2、嵌套结构体偏移量计算以及总大小

嵌套结构体时,需要先按上面的方法,计算每个单结构体的总大小,然后计算嵌套结构体的。假设已经获取到两个子结构体的总大小和最宽基本成员类型。

为了方便在服务端数据读取某个变量,可以设置全局的pos,匹配唯一的变量。

std::vector<std::pair<int, int>> structVec;
structVec.push_back(std::pair<int, int>(第一个结构体的总大小,第一个结构体的最宽基本成员类型));
structVec.push_back(std::pair<int, int>(第二个结构体的总大小,第二个结构体的最宽基本成员类型));

int offset = 0;
int memberMax = 0;
for (const auto& childStruct : structVec)
{
	offset = align(offset, childStruct.second);
	qDebug() << "struct start pos " << offset;
	offset += childStruct.first;
	qDebug() << "struct end pos " << offset;

	if (childStruct.second > memberMax)
		memberMax = childStruct.second;
}

int total = align(offset, memberMax);  // offset = 62, total = 64
3、示例
struct AA
{
	5double
	11bool
}

struct BB
{
	4bool
}

struct CC
{
	2bool
}

struct variant
{
	AA a;
	BB b;
	CC c;
}
AA start pos (当前变量的开始位置)end pos(下一个变量开始的位置)
08
816
1624
2432
3240
4041
4142
4243
4344
4445
4546
4647
4748
4849
4950
5051
BB start pos (当前变量的开始位置)end pos(下一个变量开始的位置)
01
12
23
34
CC start pos (当前变量的开始位置)end pos(下一个变量开始的位置)
01
12

再计算整体的大小。假如结构体嵌套3个子结构体时,通过计算单个结构体,前2个内部已经内存对齐了,其实结构体之间只有最末尾的需要填充字节。比如,现在是0-61的位置上有值,总共是62个字节,而为了对齐,补充2个字节,所以最后结构体的大小为64.

variant start pos (当前结构体的开始位置)end pos(下一个结构体开始的位置)
056
5660
6062


五、项目实践

1、普通版本

一、假设以下是服务端定义的结构体,传输给客户端的数据是字节流数组(一次全部传输),里面存放的是结构体信息。学校所有的年级,每个年级包含一定数目的兴趣班科目,每个科目包含人数、上课时间、或者是否人数超额。共计三层结构体

// 一年级有数学、英语
struct math {
	int person;
	double time;
}
struct english {
	int person;
	double time;
	bool studentBig;
}
struct OneGrade {
	math m;
	english e;
}

// 二年级只有英语
struct english {
	int person;
	double time;
	bool studentBig;
}
struct SecondGrade {
	english e;
}

struct School {
	OneGrade one;
	SecondGrade second;
}

二、客户端本地不通过定义结构体来直接获取数据,而是通过配置文件来获取结构体信息,通过数据结构组织。其本质是一个一个的变量,我只要按顺序定义每一个变量的类型即可(key值唯一)。通过一个结构体描述单个变量。

struct Variant {
	std::string key;  // 每个变量的key值必须唯一,为了后续能准确随机读取某个变量。
	int pos;          // 位置:大结构体、小结构体、变量。
	int size;         // 类型大小:大结构体、小结构体、变量。
	int alignment;    // 内存对齐大小:大结构体、小结构体、变量。
	int type;         // 描述类型, 假设10 struct 1 double 2 bool 3 int

	union UnionValue
	{
		int n;
		float f;
		double d;
		bool b;
	};
	VnionValue value; // 存放变量的值,用来方便存放std::any的值
	std::vector<Variant> vParam;   // 存放结构体的子结构体信息
}

std::vector<Variant> m_gradeRead;  // 存放每个年级信息,及其子结构体

三、客户端通过配置文件获取到每个变量的类型及标识key。接下来就可以组织信息到m_gradeRead中,获取到每个变量在字节数组唯一的位置。
假数据,已经添加了一年级中的两个结构体(两个兴趣班)

std::vector<Variant> gradeParam;
Variant m1;
m1.varType = 3;
m1.size = 4;
m1.alignment = 4;
Variant m2;
m2.varType = 1;
m2.size = 8;
m2.alignment = 8;
std::vector<Variant> mathParam;
mathParam.push_back(m1);
mathParam.push_back(m2);

Variant e1;
e1.varType = 3;
e1.size = 4;
e1.alignment = 4;
Variant e2;
e2.varType = 1;
e2.size = 8;
e2.alignment = 8;
Variant e3;
e3.varType = 2;
e3.size = 1;
e3.alignment = 1;
std::vector<Variant> englishParam;
englishParam.push_back(e1);
englishParam.push_back(e2);
englishParam.push_back(e3);

Variant class1;
class1.varType = 10;
class1.vParam = mathParam;
Variant class2;
class2.varType = 10;
class2.vParam = englishParam;
gradeParam.push_back(class1);
gradeParam.push_back(class2);

1、计算一年级每个兴趣班


1、先计算一年级,每个兴趣班的大小及内存对齐
for (auto& param : gradeParam)
{
	int offset = 0;
	int memberMax = 0;
	for (auto& var : param.vParam)
	{
		offset = align(offset, var.alignment);
		offset += var.size;
		if (var.aligname > memberMax)
			memberMax = var.size;
	}
	int total = align(offset, memberMax);
	param.size = total;
	param.alignment = memberMax;
}

2、计算一年级,每个兴趣班开始的位置
int offset = 0;
int memberMax = 0;
for (auto& param : gradeParam)
{
	offset = align(offset, param.alignment);
	param.pos = offset;
	offset += param.size;
	
	if (param.alignment > memberMax)
		memberMax = param.alignment;
}
int totalSize = align(offset, memberMax);  // 一年级结构体的总大小。

3、插入一年级
Variant grade;
grade.size = totalSize;
grade.alignment = memberMax;
grade.vParam = gradeParam;
m_gradeRead.push_back(grade);

2、计算二年级每个兴趣班

Variant grade;
std::vector<Variant> gradeParam;  // 假设已经添加了二年级中的一个结构体(一个兴趣班)

1、先计算二年级,每个兴趣班的大小及内存对齐
for (auto& param : gradeParam)
{
	int offset = 0;
	int memberMax = 0;
	for (auto& var : param.vParam)
	{
		offset = align(offset, var.alignment);
		offset += var.size;
		if (var.aligname > memberMax)
			memberMax = var.size;
	}
	int total = align(offset, memberMax);
	param.size = total;
	param.alignment = memberMax;
}

2、计算二年级,每个兴趣班开始的位置
int offset = 0;
int memberMax = 0;
for (auto& param : gradeParam)
{
	offset = align(offset, param.alignment);
	param.pos = offset;
	offset += param.size;
	
	if (param.alignment > memberMax)
		memberMax = param.alignment;
}
int totalSize = align(offset, memberMax);  // SecondGrade 结构体总大小。

3、插入二年级
grade.size = totalSize;
grade.alignment = memberMax;
grade.vParam = gradeParam;
m_gradeRead.push_back(grade);

3、计算每个年级开始的位置

int offset = 0;
int memberMax = 0;
for (auto& grade : m_gradeRead)
{
	offset = align(offset, grade.alignment);
	grade.pos = offset;
	offset += grade.size;
	
	if (grade.alignment > memberMax)
		memberMax = grade.alignment;
}
int totalSize = align(offset, memberMax);  // School 结构体总大小。

4、计算每个元素变量的绝对位置

// 遍历年级
for (auto& grade : m_gradeRead)
{
	// 遍历年级的兴趣班种类
	for (auto& param : grade.vParam)
	{
		int offset = 0;
		// 基准值 = 第几个年级的pos + 第几个兴趣班的pos
		int start = grade.pos + param.pos;
	
		// 遍历年级的兴趣班种类的每个元素变量
		for (auto& var : param.vParam)
		{
			offset = align(offset, var.alignment);
			var.pos = start + offset;
			offset += var.size;
		}
		offset = 0;
	}
}





2、递归版本

理论上需要先算出来所有单个元素的大小; 再计算小结构体的大小、位置; 再计算大结构体的大小、位置。
最后在计算每个元素的pos绝对位置。

但是这样比较麻烦,不通用,可以使用递归,避免处理复杂的嵌套。但可能栈溢出,开发者可自行选择只解析几层,还是多层一次解析

以下是递归版本,测试通过。

// 假设全校变量 m_gradeRead,已经存储了两个年级的信息。

// 1、所有年级的结构体、变量的大小及内存对齐。(计算除最外层的里面的大小及内存对齐)
for (auto& grade : m_gradeRead)
	GetSubStructSize(grade);  // 递归

// 2、计算每个年级的偏移量(计算最外层之间的位置)
int offset = 0;
int memberMax = 0;
for (auto& grade : m_gradeRead)
{
	offset = align(offset, grade.alignment);
	grade.pos = offset;
	offset += grade.size;
	
	if (grade.alignment > memberMax)
		memberMax = grade.alignment;
}
int totalSize = align(offset, memberMax);  // School 结构体总大小。

// 3、推导变量的位置,累加
for (auto& grade : m_gradeRead)
{
	int start = 0;
	GetSubStructPos(grade, start);  // 递归
}

计算大小

bool GetSubStructSize(Variant & subUnit)
{
    int offset = 0;
    int memberMax = 0;

    for (auto& var : subUnit.vParam)
    {
        if (var.varType == enumVariant_STRUCT)
            GetSubStructSize(var);

        offset = align(offset, var.alignment);
        var.pos = offset;
        offset += var.size;

        if (var.alignment > memberMax)
            memberMax = var.alignment;
    }

    int total = align(offset, memberMax);
    subUnit.size = total;
    subUnit.pos = offset;
    subUnit.alignment = memberMax;
    return true;
}


计算位置

bool GetSubStructPos(Variant & subUnit, int& start)
{
    int offset = 0;
    bool bHaveStruct = false;
    for (auto& var : subUnit.vParam)
    {

        if (var.varType == 10)  // 10是结构体
        {
            start = subUnit.pos + var.pos;
            var.pos = start;  // 更新变量位置
            bHaveStruct = true;
            GetSubStructPos(var, start);
        }
        
        offset = align(offset, var.alignment);
        
        // 当前结构体包含结构体和变量,则位置 = 该结构体的起始位置 + 偏移量
        if (bHaveStruct)
            var.pos = subUnit.pos + offset;
        else  // 当前结构体只包含变量,则位置 = 上一个的位置 + 偏移量
            var.pos = start + offset;

        offset += var.size;
    }
    return true;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值