关于内存对齐问题

举报
炒香菇的书呆子 发表于 2022/04/20 16:04:49 2022/04/20
【摘要】 关于内存对齐计算机内存是以字节(Byte)为单位划分的,理论上 CPU 可以访问任意编号的字节,但实际情况并非如此。对于一个数据总线宽度为32位的CPU,实际寻址的步长为4个字节,也就是只对编号为 4 的倍数的内存寻址,例如0、4、8、12、1000等,而不会对编号为 1、3、11、1001 的内存寻址。(64位的处理器也是这个道理,每次读取8个字节)。以32位CPU寻址为例:这样做可以以...

关于内存对齐

计算机内存是以字节(Byte)为单位划分的,理论上 CPU 可以访问任意编号的字节,但实际情况并非如此。
对于一个数据总线宽度为32位的CPU,实际寻址的步长为4个字节,也就是只对编号为 4 的倍数的内存寻址,例如0、4、8、12、1000等,而不会对编号为 1、3、11、1001 的内存寻址。(64位的处理器也是这个道理,每次读取8个字节)。

32位CPU寻址为例:

6D18C2F61D81AA02

这样做可以以最快的速度寻址:不遗漏一个字节,也不重复对一个字节寻址。对于程序来说,一个变量最好位于一个寻址步长的范围内,这样一次就可以读取到变量的值;如果跨步长存储,就需要读取两次,然后再拼接数据,效率显然降低了。

如下图:

6AC02D065B3E95EB

当数据在 2-5 中,32位CPU 在读取时实际上是先读取 0-3 ,然后再读取 4-7 字节,再将两次获得的数据进行合并,最后获得所需的四字节数据。再比如一个 int 类型的数据,如果地址为 8,那么很好办,对编号为 8 的内存寻址一次就可以。将一个数据尽量放在一个步长之内,避免跨步长存储,这称为内存对齐。

结构体内存对齐

为了提高存取效率,编译器会自动进行内存对齐,请看下面的代码:

#include <stdio.h>
#include <stdlib.h>

struct{
    int a;
    char b;
    int c;
}t={ 10, 'C', 20 };

int main(){
    printf("length: %d\n", sizeof(t));
    printf("&a: %X\n&b: %X\n&c: %X\n", &t.a, &t.b, &t.c);
    system("pause");
    return 0;
}

在32位编译模式下的运行结果:

length: 12
&a: B69030
&b: B69034
&c: B69038

如果不考虑内存对齐,结构体变量 t 所占内存应该为 4+1+4 = 9 个字节。考虑到内存对齐,虽然成员 b 只占用 1 个字节,但它所在的寻址步长内还剩下 3 个字节的空间,放不下一个 int 型的变量了,所以要把成员 c 放到下一个寻址步长。剩下的这 3 个字节,作为内存填充浪费掉了。

2A36A5B9B2B586B0

编译器之所以要内存对齐,是为了更加高效的存取成员 c,而代价就是浪费了3个字节的空间。

通过上面例子可以得出:结构体变量的起始地址需要让自身变量宽度整除,如果不能就需要往前面填充字节,而在计算结构体大小时还有一个规则,结构的总大小必须可以被最宽成员的大小整除,如果不能则在后面补充字节

全局变量内存对齐

除了结构体,变量也会进行内存对齐,请看下面的代码:

#include <stdio.h>
#include <stdlib.h>
int m;
char c;
int n;
int main(){
    printf("&m: %X\n&c: %X\n&n: %X\n", &m, &c, &n);
    system("pause");
    return 0;
}

运行结果:

&m: DE3384
&c: DE338C
&n: DE3388

字节对齐: 一个变量占用 n 个字节,则该变量的起始地址必须是 n 的整数倍,即:起始存放地址 % n = 0,如果不够则补齐字节

可见它们的地址都是4的整数倍并相互挨着。内存对齐虽然和硬件有关,但是决定对齐方式的是编译器,如果你的硬件是64位的,却以32位的方式编译,那么还是会按照4个字节对齐
对齐方式可以通过编译器参数修改。

设置对齐系数

在内存要求很高的时候,我们可以放弃空间换时间,改成时间换空间,我们自定义对齐,而不全部按照编译器默认对齐方式,通过 #pragma pack(n) 来改变结构体成员的对齐方式,
n 可以定义为 1、2、4、8、16

我们看例子:

// 我们通过切换系数查看不同占用大小
#pragma pack(1)
struct Struct1
{
    char a;//1byte
    int b;//4byte
    char c;//1byte
} t;

#pragma  pack()

int main(){
    printf("length: %d\n", sizeof(t));
    printf("&a: %X\n&b: %X\n&c: %X\n", &t.a, &t.b, &t.c);
    return 0;
}

输出:

length: 6
&a: 407970
&b: 407971
&c: 407975

97CCDA798F59312B

从上图可以看出,系数改变后,内存占用大小确实发生了变化吗,而规则主要是:变量宽度与对齐系数进行比较,谁小使用哪个进行对齐。例如当 pack(1) 时, a 占用 1个byteb 需要 4个byte,但与系数比较是大于系数的,所以按照 1个byte 进行对齐,则紧挨着 a 的内存, c 也是同理

系数N = Min(最大成员宽度,对齐系数),当然结构体整体大小还得是N的整数倍,不是整数倍需要补齐字节

总结

读完这篇文章可以知道

  • 内存对齐是一种空间换时间的策略
  • 结构体计算内存大小有以下规则:
    • 结构体变量的起始地址能够被其最宽的成员大小整除
    • 结构体每个成员相对于起始地址的偏移能够被其自身大小整除,如果不能则在前一个成员后面补充字节
    • 结构体总体大小能够被最宽的成员的大小整除,如不能则在后面补充字节
  • 在内存紧缺的时候还可以手动设置对齐系数,转换策略

实际上字节对齐并非真正表示这个变量实际内存大小就是对齐后的大小,变量真实大小并未改变,对齐是编译器处理的,想了解更多可以细看一下编译原理。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。