概述
目录
文章目录
- 目录
- CPU 的内存对齐
- C 语言的内存对齐
- 对齐系数
- GCC 内存对齐问题
- 使用 pragma 宏指令修改对齐系数
CPU 的内存对齐
我们知道现代 CPU 主要有 32bit 和 64bit 字长之分,指的是 CPU 的寻址能力,即:一次可以处理的数据量大小,由 CPU 和 Bus(总线)宽度共同决定。
- 32bit CPU:一次能够处理 32bit(4Byte)内存数据,内存寻址空间为 4GB。
- 64bit CPU:一次能够处理 64bit(8Byte)内存数据,内存寻址空间为 256GB。
所以,在 Linux 操作系统的虚拟地址空间中,为物理内存空间的编址和寻址实现了一种称为内存对齐(Memory Alignment)的内存访问方式,比如:32bit CPU 要求 4Byte 字节对齐,64bit CPU 要求 8Byte 字节对齐。
以下图为例,如果 32bit CPU 要访问的数据刚好为 4Byte 大小,并且刚好也是内存对齐的,那么 CPU 就只需要一次内存访问即可;相反,则需要 2 次访问(跨越了 2 个对齐空间),从而浪费了 CPU 时钟周期。
此外,现代 CPU 还支持多种高级内存访问指令,例如 SSE 和 AVX 指令,这些指令可以同时处理多个数据元素。但是这些指令要求数据必须按照特定的对齐方式排列,否则会导致程序错误或性能下降。
C 语言的内存对齐
对齐系数
对齐系数是指数据在内存中存储时,数据的起始地址,必须对齐到某个系数的字节边界上,例如:对齐系数是 4,则表示每个数据在内存中存储时,其起始地址都必须是 4 的倍数。对齐系数是造成内存空洞的主要原因,同时也是性能提升的关键原因。设置合适的对齐系数能够做到节省空间和提升性能的均衡。
默认的对齐系数跟平台有关,通常也是 C 数据类型的大小,常见的 x86 CPU 对齐系数如下图。
也可以通过测试程序来获取具体 CPU 平台的对齐系数。
#include <stdio.h>
#define BASE_TYPE_SIZE(t) printf("%12s : %2d Byte%sn", #t, sizeof(t), (sizeof(t))>1? "s": "")
void base_type_size(void)
{
BASE_TYPE_SIZE(void);
BASE_TYPE_SIZE(char);
BASE_TYPE_SIZE(short);
BASE_TYPE_SIZE(int);
BASE_TYPE_SIZE(long);
BASE_TYPE_SIZE(long long);
BASE_TYPE_SIZE(float);
BASE_TYPE_SIZE(double);
BASE_TYPE_SIZE(long double);
BASE_TYPE_SIZE(void*);
BASE_TYPE_SIZE(char*);
BASE_TYPE_SIZE(int*);
typedef struct {
} StructNull;
BASE_TYPE_SIZE(StructNull);
BASE_TYPE_SIZE(StructNull*);
}
int main()
{
base_type_size();
return 0;
}
执行结果:
void : 1 Byte
char : 1 Byte
short : 2 Bytes
int : 4 Bytes
long : 8 Bytes
long long : 8 Bytes
float : 4 Bytes
double : 8 Bytes
long double : 16 Bytes
void* : 8 Bytes
char* : 8 Bytes
int* : 8 Bytes
StructNull : 0 Byte
StructNull* : 8 Bytes
GCC 内存对齐问题
在 C 语言中,基础数据类型的大小都是以 Byte 为单位设计的,例如:char 1B、int 4B 等,这些类型数据的对齐系数通常就是其自身的大小,所以可以简易的实现内存对齐,GCC 也提供了相应的内存对齐优化特性。
但是,对于结构复杂的构造数据类型而言,例如:Struct、Union 等,这些类型数据想要实现内存对齐,则还需要花费一番心机。
因为,在默认情况下,结构体的对齐系数通常是按照结构体中最宽的成员的大小来确定的,即:结构体采用所有成员中最大的对齐系数作为自身的对齐系数。
如下例子:Struct Test1 中有 1 个 int 成员(系数为 4)和 2 个 char 成员(系数为 1),那么此时 Test1 的对齐系数就为 4。
#include <stdio.h>
struct {
int i;
char c1;
char c2;
} Test1;
struct {
char c1;
int i;
char c2;
} Test2;
struct {
char c1;
char c2;
int i;
} Test3;
int main()
{
printf("%dn", sizeof(Test1)); // 输出 8
printf("%dn", sizeof(Test2)); // 输出 12
printf("%dn", sizeof(Test3)); // 输出 8
return 0;
}
下图为 3 个结构体变量的内存布局图。
首先可以看见,这 3 个 Struct 的起始地址肯定都是对齐系数 4 的倍数。其次,可以发现,GCC 除了根据 “对齐系数“ 之外,还会根据 “成员顺序“ 来考虑如何为 char 成员进行空间填充。由于成员顺序不同,导致 3 个结构体分别为 char 填充的空间为:2Byte、6Byte、2Byte。
显然,不同的代码导致了不同的效率,这就是 GCC 带来的内存对齐问题。
使用 pragma 宏指令修改对齐系数
为了解决上述问题,C 语言提供了 pragma 宏指令,可用于修改某个结构体的对齐系数。宏指令为 #pragma pack(n),其中的 n 用于指定对齐值,通常是 2 的较小次方(e.g. 1、2、4),当为 1 的时候最节省空间,但内存访问性能有损失。
- 如果 n 的值小于默认对齐值,则按照 n 的值进行对齐。
- 如果 n 的值大于默认对齐值,则按照默认值进行对齐。
还可以通过指令 #pragma pack() 来告诉 GCC 关闭对某个结构体的内存对齐优化行为。
对于上述例子中的 3 个结构体,如果设定了 #pragma pack(1),那么根据对齐规则,就不再需要为 char 填充空间了。
如果设定了 #pragma pack(2),那么 3 个结构体的空间大小应为 6、8、6。内存分布如下图:
示例代码:
#include <stdio.h>
struct MyStruct {
char a;
int b;
};
// 使用 #pragma pack(1) 指令修改对齐系数为 1 字节
#pragma pack(1)
struct MyStruct2 {
char a;
int b;
};
int main() {
printf("sizeof(struct MyStruct) = %lun", sizeof(struct MyStruct)); // 输出为 8
printf("sizeof(struct MyStruct2) = %lun", sizeof(struct MyStruct2)); // 输出为 5
return 0;
}
最后
以上就是怕孤单银耳汤为你收集整理的C 语言编程 — GCC 内存对齐问题目录CPU 的内存对齐C 语言的内存对齐的全部内容,希望文章能够帮你解决C 语言编程 — GCC 内存对齐问题目录CPU 的内存对齐C 语言的内存对齐所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复