当前位置:首页 > 嵌入式 > 嵌入式微处理器
[导读]为什么要内存对齐 虽然所有的变量最后都会保存到特定的地址内存中去,但是相应的内存空间必须满足内存对齐的要求,主要基于存在以下两个原因: 硬件平台原因:并不是所有的平台都能够访问任意地址上的任意数据,某些硬件平台只能够访问对齐的地址,否则就会出


为什么要内存对齐

虽然所有的变量最后都会保存到特定的地址内存中去,但是相应的内存空间必须满足内存对齐的要求,主要基于存在以下两个原因:

  • 硬件平台原因:并不是所有的平台都能够访问任意地址上的任意数据,某些硬件平台只能够访问对齐的地址,否则就会出现硬件异常错误。

  • 性能原因:如果数据存放在未对齐的内存空间中,则处理器在访问变量时要做两次次内存访问,而对齐的内存访问只需要一次。

上述两个原因,第一个原因从字面意思上就能够理解,那第二个原因是什么意思呢?假定现在有一个 32 位微处理器,那这个处理器访问内存都是按照 32 位进行的,也就是说一次性读取或写入都是四字节。假设现在有一个处理器要读取一个大小为 4 字节的变量,在内存对齐的情况下,处理器是这样进行读取的:

那如果数据存储没有按照内存对齐的方式进行的话,处理器就会这样进行读取:

对比内存对齐和内存没有对齐两种情况我们可以明显地看到:在内存对齐的情况下,只需要两个个步骤就可以将数据读出来,首先处理器找到要读出变量所在的地址,然后将数据读出来。在内存没有对齐的情况下,却需要以下四个步骤才能够将数据取出来:

  • 处理器找到要读取变量所在的地址,也就是图中红色方块所在位置。

  • 由于此时内存未对齐,处理器是 32 位的,一次性读取或者写入都是 4 字节,所以需要将 0-3 地址内的数据和 4-7 地址里的数据都取出来。

  • 由于 0 - 3 地址范围的 0 地址里的数据不属于我们要读取的数据,因此将这一小块的数据进行移位,把 0 地址里的数据移出去;同理, 4 - 7 地址范围里的数据也要进行移位,保留 4 地址里的数据

  • 合并移位之后的数据,得出结果

通过上述的分析,我们可以知道内存对齐能够提升性能,这也是我们要进行内存对齐的原因之一。

结构体内存对齐

对齐原则

在明白了为何要进行内存对齐之后,我们来分析结构体内的内存对齐,在进行具体的实例分析前,需要给出结构体内存对齐的两条基本原则。

  • 结构体各成员变量的内存空间的首地址必须是“对齐系数”和“变量实际长度”中较小者的整数倍。

  • 对于结构体来说,在其各个数据都对齐之后,结构体本身也需要对齐,即结构体占用的总大小应该为“对齐系数”和“最大数据长度”中较小值的整数倍。

在给定了基本原则之后,我们通过一个例子来说明结构体的内存对齐,假定当前的处理器是 32 位的,对齐系数为4。在这里笔者选择在上一篇文章中涉及到的一个结构体进行解析,结构体如下:

   
  1. struct data_test

  2. {

  3. char a; /*本身大小 1 字节*/

  4. short b; /*本身大小 2 字节*/

  5. char c[2]; /*数组单个成员 1 字节*/

  6. double d; /*本身大小 8 字节*/

  7. char e; /*本身大小 1 字节*/

  8. int f; /*本身大小 4 字节*/

  9. char g; /*本身大小 1 字节*/

  10. }data;

根据我们刚刚给出的第一条对齐原则,先确定出每个变量的存储位置,变量存储方式是小端对齐,为了看起来更加形象,以 16 个字节作为一行来表示变量的存储位置(这里所说的存储位置是指相对于结构体起始地址的偏移)。

根据第一条规则:各成员的内存空间的首地址必须是对齐系数和变量本身大小较小者的整数倍,这里对齐系数是 4,因此变量 a 、数组 c 、变量 e 、变量 g 的首地址需要满足 1 的倍数,变量 b 的首地址需要满足 2 的倍数,变量 d 的首地址需要满足 4 的倍数,变量 f 的首地址需要 4 的倍数。所以也就有了上述表格中的变量存储位置。那既然结构体内的成员都已经对齐了,为什么还存在第二条原则呢?也就是说为什么结构体内的成员已经内存对齐了,结构体本身还需要对齐?下面通过一个结构体数组来说明,比如我们定义了这样一个结构体数组:

   
  1. struct data_test

  2. {

  3. char a; /*本身大小 1 字节*/

  4. short b; /*本身大小 2 字节*/

  5. char c[2]; /*数组单个成员 1 字节*/

  6. double d; /*本身大小 8 字节*/

  7. char e; /*本身大小 1 字节*/

  8. int f; /*本身大小 4 字节*/

  9. char g; /*本身大小 1 字节*/

  10. }data[2];

我们在放置成员存储位置的时候,data[0] 按照成员对齐的原则依次存放,放到最后一个结构体成员时,如果不考虑结构体本身的对齐,按照数组元素是紧挨着存放的原则,那这个结构体数组应该是按照下图进行存储的:

从上图中我们可以看到虽然 data[0] 中的成员都对齐了,但是由于结构体本身的不对齐,导致 data[1] 中的好多成员都不对齐了,因此,在完成了结构体成员的内存对齐后,我们还需要依据第二条原则:结构体占用的总大小应该为“对齐系数”和“最大数据长度”中较小值的整数倍,来对结构体本身进行对齐,因此正确的结构体数组的存储位置应该如下图所示:

这里需要注意的是,上述原则针对的是结构体占用的总大小,而不是结构体的首地址,所以,在结构体本身还没有对齐的情况下,data[0] 的大小是 25 个字节,但是根据上述原则,在对齐系数为 4 的前提下,结构体大小应该是 4 的整数倍,所以要对结构体进行所占内存进行填充,因此:data[0] 最终的大小是 28 字节,结构体数组 data 的大小为 56 字节

结构体内成员顺序

通过上述分析我们可以很容易就想到,根据第一条原则,那么结构体成员定义的先后顺序会对最终结构体占用的内存造成影响,比如现在调整结构体 data 内成员的定义顺序,如下:

   
  1. struct data_test

  2. {

  3. char a; /*本身大小 1 字节*/

  4. char c[2]; /*数组单个成员 1 字节*/

  5. char e; /*本身大小 1 字节*/

  6. char g; /*本身大小 1 字节*/

  7. short b; /*本身大小 2 字节*/

  8. int f; /*本身大小 4 字节*/

  9. double d; /*本身大小 8 字节*/

  10. }data;

改变结构体成员顺序后的存储位置如下:

通过图片就可以看到只有一块蓝色的填充区域,在成员对齐之后,结构体大小是 20 ,已经是 4 的整数倍,已经无须再做填充,所以调整顺序后的结构体大小为 20 个字节,相比于之前没有改变顺序之前整整减少了 8 个字节,也可以看出结构体成员的定义顺序也是需要关注的一个问题,关于结构体内成员定义的顺序应该遵循这样一个原则:按照长度递增的顺序依次定义各个成员

如何设定对齐系数

查看默认对齐系数

在上述我们对结构体内存对齐的分析中,我们都是假定对齐系数为 4 ,实际上对于编译器来说都有默认的对齐系数,我们可以输入伪指令,然后以报警信息的方式显示当前的对齐系数:

   
  1. #pragma pack(show)

设置对齐系数

   
  1. #pragma pack(1) /*设置一字节对齐*/

  2. struct data_test

  3. {

  4. char a;

  5. short b;

  6. char c[2];

  7. double d;

  8. char e;

  9. int f;

  10. char g;

  11. } data;

  12. #pragma pack()/*取消一字节对齐,恢复默认对齐系数*/

在这里,设置1字节对齐其实也就相当于不进行内存对齐,因为任何地址都可以是 1 的整数倍,最后,需要注意的是使用这种方法设置字节对齐,要在想要取消一字节对齐的地方使用伪指令 #pragma pack() 取消一字节对齐,否则后面所定义的结构体会继续采用刚刚所设置的对齐方式。除了采用上述这样设置一字节对齐的方式取消内存对齐,也可以采用下面的方式取消字节对齐:

   
  1. struct __attribute__((packed)) data_test2

  2. {

  3. char a;

  4. short b;

  5. char c[2];

  6. double d;

  7. char e;

  8. int f;

  9. char g;

  10. }data;

这种方式相对于上述方法来讲,不用执行取消操作,作用域只是本结构体,不会影响其他结构体的对齐方式。最后,取消字节对齐的结构体(或者说是按照 1 字节对齐的结构体)data 的大小就是 19 个字节,即将结构体内的所有成员的字节大小相加即可。

总结

了解结构体的内存对齐,从而在定义结构体成员时按照最优的顺序进行定义,对于 RAM 资源比较紧缺的 MCU 来讲,也是非常重要的。同时,在笔者的上篇文章《union 的概念及在嵌入式编程中的应用中》,所讲到的运用 union 和 struct 嵌套来便捷地解析数据,也应该取消字节对齐(因为上篇文章最后一个例子结构体成员大小都是一个字节,内存对齐取消与否都不影响成员的存储位置,所以没取消)。

参考资料 [1] https://aticleworld.com/data-alignment-and-structure-padding-bytes/

[2] https://blog.51cto.com/zhangyu/673792

本文授权转载自公众号“wenzi嵌入式软件”,作者wenzid

-END-




推荐阅读



【01】工程师:这道题80%初学者都没做对!你确定搞懂结构体内存对齐了?
【02】C语言之结构体就这样被攻克了!(绝对值得收藏的文章)
【03】为什么C语言函数不能返回数组,却可以返回结构体
【04】C语言结构体(struct)最全的讲解(万字干货)
【05】c语言基础语法——结构体


免责声明:整理文章为传播相关技术,版权归原作者所有,如有侵权,请联系删除

免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

嵌入式ARM

扫描二维码,关注更多精彩内容

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
换一批
延伸阅读

LED驱动电源的输入包括高压工频交流(即市电)、低压直流、高压直流、低压高频交流(如电子变压器的输出)等。

关键字: 驱动电源

在工业自动化蓬勃发展的当下,工业电机作为核心动力设备,其驱动电源的性能直接关系到整个系统的稳定性和可靠性。其中,反电动势抑制与过流保护是驱动电源设计中至关重要的两个环节,集成化方案的设计成为提升电机驱动性能的关键。

关键字: 工业电机 驱动电源

LED 驱动电源作为 LED 照明系统的 “心脏”,其稳定性直接决定了整个照明设备的使用寿命。然而,在实际应用中,LED 驱动电源易损坏的问题却十分常见,不仅增加了维护成本,还影响了用户体验。要解决这一问题,需从设计、生...

关键字: 驱动电源 照明系统 散热

根据LED驱动电源的公式,电感内电流波动大小和电感值成反比,输出纹波和输出电容值成反比。所以加大电感值和输出电容值可以减小纹波。

关键字: LED 设计 驱动电源

电动汽车(EV)作为新能源汽车的重要代表,正逐渐成为全球汽车产业的重要发展方向。电动汽车的核心技术之一是电机驱动控制系统,而绝缘栅双极型晶体管(IGBT)作为电机驱动系统中的关键元件,其性能直接影响到电动汽车的动力性能和...

关键字: 电动汽车 新能源 驱动电源

在现代城市建设中,街道及停车场照明作为基础设施的重要组成部分,其质量和效率直接关系到城市的公共安全、居民生活质量和能源利用效率。随着科技的进步,高亮度白光发光二极管(LED)因其独特的优势逐渐取代传统光源,成为大功率区域...

关键字: 发光二极管 驱动电源 LED

LED通用照明设计工程师会遇到许多挑战,如功率密度、功率因数校正(PFC)、空间受限和可靠性等。

关键字: LED 驱动电源 功率因数校正

在LED照明技术日益普及的今天,LED驱动电源的电磁干扰(EMI)问题成为了一个不可忽视的挑战。电磁干扰不仅会影响LED灯具的正常工作,还可能对周围电子设备造成不利影响,甚至引发系统故障。因此,采取有效的硬件措施来解决L...

关键字: LED照明技术 电磁干扰 驱动电源

开关电源具有效率高的特性,而且开关电源的变压器体积比串联稳压型电源的要小得多,电源电路比较整洁,整机重量也有所下降,所以,现在的LED驱动电源

关键字: LED 驱动电源 开关电源

LED驱动电源是把电源供应转换为特定的电压电流以驱动LED发光的电压转换器,通常情况下:LED驱动电源的输入包括高压工频交流(即市电)、低压直流、高压直流、低压高频交流(如电子变压器的输出)等。

关键字: LED 隧道灯 驱动电源
关闭