内存对齐(C/C++) 港控/mmm° 2024-02-20 11:40 35阅读 0赞 内存对齐是操作系统为了可以让 CPU 高效读取内存中的数据采用的一种策略,它使得数据在内存中都按一定的规则排列。本篇来解释一下内存对齐。 ### 为什么需要对齐 ### 计算机一般使用字节(byte)作为最小可寻址的内存单位,内存中的每个字节都有一个唯一的数字标识,也就是它的地址了,通常使用一个十六进制的数来表示。如果是占用多个字节内存的数据,则它的地址就是所使用字节中最小的地址。 虽然字节是最小可寻址单位,但是并不是 CPU 的最小读取单位。CPU 从内存中读取数据时需要通过 cache 来作为中间层,会根据目标地址首先在 cache 中找,如果找不到,就会首先从内存加载数据到 cache 中,然后再读取。单次从内存加载到 cache 的数据大小叫做 **cache line**,它的大小跟硬件有关,一般是 16 到 256 字节。在 Linux 下可以很方便查询这个值,下面给出两种查询方法,可以看到在我本地的平台上这个长度是 64 字节。 $ cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size 64 $ getconf LEVEL1_DCACHE_LINESIZE 64 CPU 在使用数据时,需要再次从 cache 中加载内容到寄存器里,这个步骤每次加载的最大长度是一个**字长**(word size),字长的大小等于我们常说的 xx 位 CPU 的那个数字,常用的 64 位 CPU 的字长就是 64 比特,也就是 8 个字节。当要处理大于 8 个字节的数据时,CPU 需要多次加载处理。 到这里铺垫已经结束了,但是还没有回答真正的问题,因为不管一次读多少,只要按字节来读就可以了,并不需要对齐。比如一个 long 类型变量地址是 6,长度是 8,那 CPU 从 6 开始读 8 个字节就可以了,并不会有什么问题。 真正导致需要内存对齐的原因是,内存条的编址方式导致了 CPU 无法从指定位置开始访问。因为内存条的 IO 采用了多个颗粒并行执行,逻辑上连续的 8 个字节其实是分布在 8 个不同的 bank 上,并行读取可以为内存的访问加速。内存条寻址的规则就是以 8 个字节为单位的,这就导致了 CPU 也只能以 8 个字节为粒度进行读取,且起始地址也要是 8 的倍数。来看一个例子。 ![54431b5a7e52468a915145f614d5b2f5.png][] 如上图所示,假设一个占用 8 字节的 long 类型整数存放在内存中,如果使用第一张图中的存放方式的话,那么在 64 位环境下,需要读取两次,分别是 0 - 7 字节 和 8 - 15 字节,两次组合才能得到该数字。如果它对齐到 8 的倍数的地址,那么一次读取即可完成。 ### 实现 ### #### 对齐的规则 #### 内存对齐的规则大概可以总结为四点: 1. 对于内置类型,只要它的地址是它长度的整数倍即可。 2. 对于 struct,其中的每个数据都要对齐,struct 本身也要向其中最大的那个数据对齐。 3. 对于 union,按照其中最长的那个数据对齐。 4. 对于数组,无需特殊处理,因为其中每一个数据都对齐了,数组本身就对齐了。 #### 对齐的操作 #### 在 struct 中,如果内存紧密排布可能会出现其中一些数据无法对齐的情况。 struct s{ int i; double d; }; 比如结构体 s,如果紧密排布的话,i 占用 0 - 3 字节,d 占用 4 - 11 字节,这样就导致了 d 没有对齐。所以会在 i 和 d 之间增加一个填充部分(padding),长度为 4 字节,使 i 和 d 都对齐。![30aca93310574bae80197071cac1037d.png][] 可以看到填充部分的增加使结构体中每一个数据都对齐了,但是也浪费了一部分的内存。当使用 sizeof 查询 s 的长度时,得到的结果是 16 字节。 struct s{ double d; int i; }; 如果将 d 和 i 的顺序反过来,情况会有所不同,虽然看起来 d 和 i 都对齐了,但是它本身的长度为 12 字节,没有对 8 字节对齐,需要在它的末尾追加一个 8 字节的填充部分。 ![88c7c9802a52470f8a9a2607077548d7.png][] 使用 sizeof 查询 s 的长度可以发现它的长度依然是 16 字节。 ### 优化内存占用 ### 可以看到因为填充部分的存在,如果我们设计 struct 时随意写数据的顺序可能会造成一定程度的空间浪费,而当熟悉对齐的规则时就可以排布出一个最省内存的顺序。来看一道 CSAPP 上的题目。 struct{ char *a; short b; double c; char d; float e; char f; long g; int h; }rec; rec 当前以 8 字节对齐,长度为 56 字节。其中为了让 c 对齐在 b 和 c 之间有一个 6 字节的填充,为了让 e 对齐在 d 和 e 之间有一个 3 字节的填充,为了让 g 对齐在 f 和 g 之间有一个 7 字节的填充,为了让整个 rec 对齐在 h 后面还有一个 4 字节的填充。一共 36 字节的结构,多使用了 20 字节的填充,浪费率比较惊人。下面来重排一下它的顺序。 struct rec{ char *a; double c; long g; float e; int h; short b; char d; char f; }; 经过重排以后,可以发现数据之间已经完全不需要填充部分了,不过因为 rec 要向 8 字节对齐,所以最后的 4 字节填充是必不可少的。重排以后的长度为 40 字节,减少了 16 字节的空间浪费。 ### 我不想对齐行不行 ### 可以看到内存对齐在大部分情况下不管怎么优化,都会浪费一部分空间。它虽然提升了一点内存访问的速度,但是可能很多时候我们并不十分在乎,反而更在意内存的额外占用。 早年间的一些 CPU 是不支持没有对齐的内存访问的,但是在现代 CPU 上已经没有了这个问题,编译器也提供了一些选项可以让我们明确指出不需要内存对齐。 struct __attribute__((packed)) testc { int i; char c; }; 如果加上了 **attribute**((packed)),则编译器会使内存紧密排布,去掉所有对齐的优化。这个时候再输出类的长度,可以发现它的长度是 5,就是一个 int 加上一个 char 的长度。如果去掉紧密排布的声明,则类的长度是 8,char 后面会增加 3 个字节的填充。 让类的空间密集排布一定会影响内存的读取效率,但是实际测试一下就会发现,这个影响其实非常非常有限,基本上可以忽略不计了。所以如果对内存占用更敏感的程序,可以主动放弃内存对齐,比如 Redis 的 sds 的结构体就全都是紧密排布的。 ### 指定对齐的字节数 ### 默认情况下类内的对齐是按照本类中最大长度的成员变量对齐的,可以指定让一个类向一个固定的值对齐。 struct __attribute__((aligned(8))) testc { char c1; int i; char c2; }; 如果没有指定对齐到 8 字节的话,testc 的长度应该是 (1 + 3) + 4 + (1 + 3) = 12,当指定向 8 字节对齐以后,testc 的长度就是 (1 + 3) + 4 + (1 + 7) = 16。 需要注意的是,指定的长度是整个结构对齐的长度。即使指定了对齐的长度,c1 和 i 之间的填充部分还是只填到 i 长度的整数倍就可以了,只是最后整个 testc 需要对齐的 8 字节的倍数,所以向后补充了 7 个字节。 [54431b5a7e52468a915145f614d5b2f5.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/02/20/61a896921d014b0da9bfd6c8740e6ef8.png [30aca93310574bae80197071cac1037d.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/02/20/0fd1213a3b024a7694466238c7a718e7.png [88c7c9802a52470f8a9a2607077548d7.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/02/20/dab71b86bbec4193929e00e39118d03b.png
相关 关于内存对齐 什么是内存对齐 考虑下面的结构: struct foo \{ char c1; short s; char c2; int i; \}; 心已赠人/ 2022年09月18日 05:41/ 0 赞/ 154 阅读
相关 详解内存对齐 `欢迎大家点击上方文字「Golang梦工厂」关注公众号,设为星标,第一时间接收推送文章。` 前言 > 哈喽,大家好,我是`asong`。好久不见,上周停更了一周,因为工 ゝ一纸荒年。/ 2022年09月05日 04:16/ 0 赞/ 184 阅读
相关 内存对齐 typedef struct { char flag[3]; short value; } sampleStruct Bertha 。/ 2022年08月04日 05:17/ 0 赞/ 160 阅读
相关 内存对齐方式 一般我们编程的时候short s=2,int i=4,char c=’a’在内存中是如何对齐的呢? 在32位系统中: ![这里写图片描述][SouthEast] 在 野性酷女/ 2022年06月09日 09:26/ 0 赞/ 244 阅读
相关 C++内存 内存对齐 1 引言 操作系统的内存分配问题与内存对齐问题对于低层程序设计来说是非常重要的,对内存分配的理解直接影响到代码质量、正确率、效率以及程序员对内存使用情况、溢出、泄露等的判断力 男娘i/ 2022年06月08日 05:07/ 0 赞/ 217 阅读
相关 C++内存对齐 内存对齐 在我们的程序中,数据结构还有变量等等都需要占有内存,在很多系统中,它都要求内存分配的时候要对齐,这样做的好处就是可以提高访问内存的速度。 我们还 Bertha 。/ 2022年06月08日 05:07/ 0 赞/ 182 阅读
相关 内存对齐 [详解见CSAPP—-计算机基础—内存对齐讲解,非常清楚一看就懂了][CSAPP_-] [http://blog.csdn.net/cosmoslife/article/de 骑猪看日落/ 2022年06月07日 22:47/ 0 赞/ 169 阅读
相关 内存字节对齐 为什么会有内存对齐 以下内容节选自《Intel Architecture 32 Manual》。 字,双字,和四字在自然边界上不需要在内存中对齐。(对字,双字,和 小鱼儿/ 2022年03月19日 04:38/ 0 赞/ 256 阅读
相关 内存对齐 每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。程序员可以通过预编译命令\pragma pack(n),n=1,2,4,8,16来改变这一系数,其中的n就是你 冷不防/ 2022年01月09日 09:21/ 0 赞/ 231 阅读
相关 内存对齐 首先由一个程序引入话题://环境:vc6 + windows sp2 程序1: include "stdafx.h" include <iostream> 我会带着你远行/ 2021年06月11日 15:10/ 0 赞/ 439 阅读
还没有评论,来说两句吧...