C语言结构体内存对齐
前言
内存对齐是处理器为了提高处理性能而对存取数据的起始地址的一种约束。以下图中的结构体定义为例:

在32位体系结构上,处理器每一次内存存取操作都是32位,即4个字节。在未采用内存对齐的情况下,处理器访问成员member1无论何时只需要一次内存存取操作,但对于成员member2却需要进行两次;但在采用内存对齐的情况下,处理器单独访问成员member1和member2都各需要一次。因此可以知道,访问未对齐的内存数据,会增加处理器访问内存的次数,频繁的存取会对程序性能造成影响。为了提高程序运行的效率,编译器会默认对程序进行内存对齐等优化。
自然边界
对于单字节类型,其任何时候都位于自然边界上;对于更长的数据类型,如字、双字和四字等,其自然边界是可以被相应数据类型大小所整除的内存地址,若其恰好位于自然边界上,则不需要进行内存对齐。此外,对于一个数据类型,若其起始地址是奇数,但是没有跨越其自然边界,仍然会被认为是对齐,原因是使用一次访存操作即可完成存取。
结构体内存对齐
结构体内每个数据成员应尽可能地对齐到对应成员类型的自然边界,以提高内存访问效率。为了实现这点,结构体成员在内存中排列时需要遵循一些规则:
- 结构体中第一个数据成员放置在偏移量为0的地址处;
- 结构体内的每个成员按照自身类型的对齐参数(类型大小的整数倍)进行对齐。具体来说就是,每个成员按照其类型的对齐参数和指定对齐参数中较小的一个进行对齐;
- 对于嵌套的结构体成员,按照自己的最大对齐参数进行对齐;
- 结构体所占用的内存长度是使用过的所有对齐参数(包含嵌套结构体的对齐参数数),即最大对齐参数的整数倍,不够就补空字节。
结构体对齐示例
参考下图所定义的结构体,在64位处理器上,使用gcc编译器默认对齐策略时的内存布局:

其中,c_val成员作为单字节数据类型,不需要考虑内存对齐;后续定制地成员s_val、l_val和i_val以其匹配类型的对齐参数进行内存对齐,其中最大对齐参数是8(long整型数确定);结构体占用内存的长度按最大对齐参数8的整数倍进行计算为24个字节,末尾补充了4个空字节。
内存对齐的空间问题
依旧参考上面示例,计算在不使用内存对齐时结构体占用的空间为15个字节,对比可知,编译器将结构中每个成员都进行内存对齐之后,整个结构体占用的空间也大幅增加。为了减小内存对齐所带来的空间消耗问题,可以调整结构体内成员的布局。稍稍修改下上面的结构体定义:

调整后的结构体在内存中的布局如上图所示,占用内存大小为16个字节,相对优化前,有效地提高了内存的使用率。后续我们在设计结构体时,也需要尽可能地将内存对齐所带来的影响考虑在内,这对系统空间分析优化将会很有帮助。
GCC的内存对齐扩展
GCC编译器的__attribute__扩展支持aligned子项以允许程序开发人员自行对结构体的对齐参数进行调整,其基本语法格式如下:
语法格式:__attribute__((aligned(n)))
aligned子项的作用是让所修饰的结构成员对齐在n字节自然边界上。如果结构中有成员的长度大于n,则按照最大成员的长度来对齐。使用示例:
#include <stdlib.h>
#include <stdio.h>
typedef struct struct_memalign_s {
char char_val;
int int_val;
long long_val;
short short_val;
} __attribute__((aligned(8))) struct_memalign_t;
int main()
{
printf("Size of struct_memalign_t is %lu.\n", sizeof(struct_memalign_t));
return 0;
}
基于x86_64体系Linux系统下,执行上述程序的结果是:
Aspiresky ~/test_struct_mem_align # ./a.out
Size of struct_memalign_t is 24.
实际上即使程序中没有使用__attribute__((aligned(8)))对结构体进行修饰,执行的结果仍然是24,原因是GCC编译器默认就会对结构体进行内存对齐的优化。如果需要取消编译器默认的对齐优化,可以使用__attribute__((packed)),使用该选项后,再次执行上述程序的结果:
Aspiresky ~/test_struct_mem_align # ./a.out
Size of struct_memalign_t is 15.
这里可以看到程序返回的是结构体内成员实际占用的字节数。
相关参考
- 《C语言深度剖析》
- 《专业嵌入式软件开发》

浙公网安备 33010602011771号