在 C
语言中,内存对齐(Memory Alignment
)是影响程序性能的重要因素之一。对齐不仅影响访问速度,还可能影响程序的正确性,特别是在硬件架构要求严格对齐的系统上。GNU C
编译器(GCC
)提供了一种方便的机制来控制内存对齐:__attribute__((aligned(alignment)))
。通过这个属性,我们可以控制变量、结构体或数组的对齐方式,以便优化性能或满足特殊需求。
本篇文章将深入介绍 __attribute__((aligned(alignment)))
的工作原理,并通过代码实例来演示它的应用场景。
内存对齐是指变量的地址满足特定的边界要求。通常,编译器会根据目标平台的要求自动分配对齐。例如,在 32
位系统上,int
类型的变量一般按 4
字节对齐,而 double
可能按 8
字节对齐。
通过对齐,可以保证 CPU
在读取或写入数据时,能够在最少的指令周期内完成访问。如果变量没有对齐到合适的边界,可能需要额外的指令进行内存操作,导致性能下降。
__attribute__((aligned(alignment)))
是 GCC
提供的一个属性,用于手动设置变量或结构体的对齐方式。它允许你指定对齐边界,单位是字节。该属性可以在变量声明或类型定义时使用。它的作用包括:
SSE、AVX
)要求数据按 16
字节或 32
字节对齐。__attribute__((aligned))
能帮助数据满足这些指令的对齐需求。__attribute__((aligned(alignment)))
使用的基本语法如下:
type variable __attribute__((aligned(alignment)));
其中:
type
是变量类型。variable
是变量名。alignment
是对齐的字节数,通常是 2
的 n
次幂,n
为正整数,最小对齐单位为 2
字节。例如 2、4、8、16、32
等。为 aligned
前后加上下划线,这允许您在头文件中使用这些属性,而不必关心可能同名的宏。例如,你可以使用 __aligned__
代替 aligned
:
__attribute__ ((aligned(8))) 等同于 __attribute__ ((__aligned__(8)))
如果未指定 alignment
,编译器会选择合适的对齐值,通常是变量类型的默认对齐方式。
源文件 test.c
:
#include
#include
struct NoAlign {
char a; // 1 字节
int b; // 4 字节
};
struct WithAlign {
char a; // 1 字节
int b __attribute__((aligned(8))); // 4 字节,强制 8 字节对齐
};
int main() {
// 打印结构体成员的偏移量以及整个结构体的大小
printf("NoAlign 结构体中:\n");
printf(" a 的偏移量: %zu\n", offsetof(struct NoAlign, a));
printf(" b 的偏移量: %zu\n", offsetof(struct NoAlign, b));
printf(" NoAlign 的大小: %zu\n", sizeof(struct NoAlign));
printf("\nWithAlign 结构体中:\n");
printf(" a 的偏移量: %zu\n", offsetof(struct WithAlign, a));
printf(" b 的偏移量: %zu\n", offsetof(struct WithAlign, b));
printf(" WithAlign 的大小: %zu\n", sizeof(struct WithAlign));
return 0;
}
上述代码解析如下:
结构体 NoAlign
char a
是 1
字节的字符类型。int b
是 4
字节的整数类型,通常在大多数平台上按 4
字节对齐。编译器为了让 int b
对齐到 4
字节,会在 char a
和 int b
之间插入 3
字节的填充。由于 b
是 4
字节对齐,所以整个结构体的大小也会被调整。
【拓展:结构体对齐规则】
1、第一个成员在与结构体变量偏移量为
0
的地址处。2、其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。 (对齐数:结构体成员自身大小和默认对齐数的较小值)
VS
上默认对齐数是8
,gcc
没有默认对齐数,对齐数就是变量本身的大小。3、结构体总大小为最大对齐数(每个成员变量都有自己的对齐数)的整数倍。
4、针对嵌套结构体,嵌套的结构体要对齐到自己最大对齐数的整数倍处,结构体总大小是所有对齐数的最大值(包含嵌套结构体的对齐数)的整数倍。
结构体 WithAlign
char a
是 1
字节的字符类型。int b
被强制要求使用 __attribute__((aligned(8)))
设置为 8
字节对齐。即使 int
默认是按 4
字节对齐,这里我们强制将其对齐到 8
字节边界。编译器会在 char a
和 int b
之间插入 7
字节 的填充,以保证 b
的地址是 8
字节的倍数。并且,整个结构体的大小也必须是 8
的倍数。
编译运行上述程序,如下:
[root@localhost alianed]# gcc -o test test.c
[root@localhost alianed]# ls
test test.c
[root@localhost alianed]# ./test
NoAlign 结构体中:
a 的偏移量: 0
b 的偏移量: 4
NoAlign 的大小: 8
WithAlign 结构体中:
a 的偏移量: 0
b 的偏移量: 8
WithAlign 的大小: 16
NoAlign 结构体分析
a
的偏移量是 0
,因为 a
位于结构体的起始位置。b
的偏移量是 4
,因为编译器在 a
后面插入了 3
个字节的填充,使 b
对齐到 4
字节边界。8
字节,这是因为它的成员及其填充后总共占用了 8
字节的空间。WithAlign 结构体分析
a
的偏移量是 0
,同样位于结构体的起始位置。b
的偏移量是 8
,因为我们强制要求 b
按 8
字节对齐,因此在 a
后面插入了 7
字节的填充。16
字节。由于 b
按 8
字节对齐,结构体的大小也必须是 8
字节的倍数,所以编译器在 b
后面补齐使结构体达到 16
字节。通过上面的结果和分析,我们可以得出如下结论:
__attribute__((aligned))
修饰时,编译器会根据默认的类型对齐规则分配内存,并插入必要的填充。__attribute__((aligned))
修饰时,我们可以强制改变对齐方式,编译器会根据指定的对齐值插入填充字节,并调整结构体的大小。通过查看输出中的偏移量和结构体大小,可以明显看出使用 __attribute__((aligned))
后,内存布局发生了变化,尤其是当对齐要求大于成员类型的默认对齐时,填充字节的数量会显著增加。
源文件 test_struct.c
:
#include
#include
struct S2 {
short b[3]; // 一个包含 3 个 short 元素的数组
int a; // 一个 int 类型变量
char c; // 一个 char 类型变量
} __attribute__((aligned(8))); // 按照 8 字节对齐
int main() {
// 打印结构体成员的偏移量以及整个结构体的大小
printf("S2 结构体中:\n");
printf(" b 的偏移量: %zu\n", offsetof(struct S2, b));
printf(" a 的偏移量: %zu\n", offsetof(struct S2, a));
printf(" c 的偏移量: %zu\n", offsetof(struct S2, c));
printf(" S2 的大小: %zu\n", sizeof(struct S2));
return 0;
}
上述代码解析如下:
结构体 S2
short b[3]
: 这是一个包含 3
个 short
类型元素的数组。short
的大小通常是 2
字节,因此 b
数组的大小为 3 * 2 = 6
字节。int a
: 这是一个 int
类型的变量,通常是 4
字节大小,默认按 4
字节对齐。char c
: char
只占用 1
字节,没有对齐要求。前面的 a
占用了 4
字节,因此 c
的偏移量紧接在 a
之后,即在 12 字节 处。__attribute__((aligned(8)))
: 该属性强制 S2
结构体的对齐要求为 8
字节。即 S2
的起始地址和其所有成员的偏移量,必须是 8
的倍数。【拓展:offsetof】
offsetof()
是一个宏,用来计算结构体中某个成员相对于结构体起始地址的偏移量。它的实现通过假设结构体的基地址为0
来计算成员的地址偏移。原型如下:#define offsetof(type, member) ((size_t) &(((type *)0)->member))
参数说明
type
:结构体类型。member
:结构体中的成员名。返回值
offsetof()
返回的是member
成员相对于结构体起始地址的字节偏移量。返回的偏移量是一个无符号整数类型size_t
。
代码解析
int main() {
// 打印结构体成员的偏移量以及整个结构体的大小
printf("S2 结构体中:\n");
printf(" b 的偏移量: %zu\n", offsetof(struct S2, b));
printf(" a 的偏移量: %zu\n", offsetof(struct S2, a));
printf(" c 的偏移量: %zu\n", offsetof(struct S2, c));
printf(" S2 的大小: %zu\n", sizeof(struct S2));
return 0;
}
offsetof(struct S2, b)
b
是结构体的第一个成员,数组 short b[3]
。数组 b
的起始地址与结构体 S2
的起始地址相同,因此 b
的偏移量是 0
字节。offsetof(struct S2, a)
a
是结构体的第二个成员,类型为 int
。我们首先知道 b[3]
占用了 6
字节(3 * sizeof(short) = 6
字节)。
默认情况下,int a
按 4
字节对齐,但由于 S2
结构体整体按 8
字节对齐,因此 a
必须按 8
字节对齐。为了满足 a
的 8
字节对齐要求,编译器会在 b[3]
之后插入 2 字节 的填充。所以 a
的偏移量是 8
字节,而不是 6
字节。
offsetof(struct S2, c)
char
只占用 1
字节,没有对齐要求。前面的 a
占用了 4
字节,因此 c
的偏移量紧接在 a
之后,即在 12 字节 处。因为结构体 S2
被要求按 8
字节对齐,最后还会插入填充字节来保证结构体的大小是 8
字节的倍数。sizeof(struct S2)
8
字节对齐的要求。b[3]
占用 6
字节。2
字节填充,使 a
对齐到 8
字节边界。a
占用 4
字节。c
占用 1
字节。为了满足结构体整体按 8
字节对齐的要求,编译器会在最后再插入 5 字节填充,使总大小为 16
字节。所以,sizeof(struct S2)
的结果是 16 字节。编译运行上述程序,如下:
[root@localhost alianed]# ./test_struct
S2 结构体中:
b 的偏移量: 0
a 的偏移量: 8
c 的偏移量: 12
S2 的大小: 16
__attribute__((aligned(8)))
修饰结构体时,主要作用是强制整个结构体按照 8
字节对齐,而不一定要求结构体内的每个成员都按照 8
字节对齐。具体而言:
结构体本身的对齐:结构体的起始地址会强制按照 8
字节对齐。这意味着当该结构体实例在内存中分配时,它的起始地址会是 8
的倍数。
结构体内部成员的对齐:每个成员依然根据其自身的数据类型以及默认的对齐要求进行对齐,但会考虑 __attribute__((aligned(8)))
对结构体整体对齐的影响。一般情况下,编译器会根据成员的类型和自身的对齐要求来安排它们的内存位置,插入必要的填充字节。
结构体大小的对齐:最终,结构体的大小会被调整为 8
的倍数。如果结构体内部的成员及其对齐填充导致结构体的大小不是 8
的倍数,编译器会在结构体的末尾插入填充字节,使整个结构体的大小满足 8
字节对齐的要求。
综合前面,可以得出结论:
8
字节对齐。8
字节对齐,编译器可能会在必要时在成员之间或末尾插入填充字节。源文件 aligned.c
:
#include
#include
void test()
{
struct S
{
int a;
char b[5];
int c;
};
printf("结构体S的大小为:%d \n", sizeof(struct S));
struct S1
{
int a;
char b;
};
printf("结构体S1的大小为:%d \n", sizeof(struct S1));
struct S2
{
int a;
char str[5];
struct S s __attribute((aligned(8))) ;
};
printf("\nS2 结构体中:\n");
printf("a 的偏移量: %zu\n", offsetof(struct S2, a));
printf("str 的偏移量: %zu\n", offsetof(struct S2, str));
printf("s 的偏移量: %zu\n", offsetof(struct S2, s));
printf("结构体S2的大小为:%d \n", sizeof(struct S2));
}
int main()
{
test();
return 0;
}
这段代码演示了几个结构体的内存布局和对齐,这里重点分析嵌套结构体的对齐方式,尤其是通过 __attribute__((aligned(8)))
设置的强制对齐。上述代码解析如下:
结构体 S
:int a
占用 4
字节,char b[5]
占用 5
字节,int c
占用 4
字节。结构体 S
的总大小为 16
字节(并不是 13
字节,因为编译器为 int c
对齐填充了 3
字节的空隙,使结构体大小对齐到 4
的倍数)。
结构体 S1
:int a
占用 4
字节,char b
占用 1
字节。这里的总大小是 8
字节(而不是 5
字节),因为 char b
之后,编译器会插入 3
字节的填充,以确保结构体大小为最大对齐数 int
的倍数(即 4
字节的倍数),以提高对齐效率。
结构体 S2
:int a
占用 4
字节,从偏移量 0
开始;char str[5]
紧接在 a
后面,占用 5
字节,偏移量为 4
。由于 str
的结束地址是 9
(从偏移量 4
开始), struct S
的 __attribute__((aligned(8)))
指定了 8
字节对齐,因此它的偏移量为 16
,并且它的大小仍然为 16
字节。为了让 struct S
的地址对齐到 16
(8
的倍数),编译器插入了 7
个字节的填充(确保 s
的偏移量为 16
),故 struct S
的结束地址是 31
。由于结构体总大小是所有对齐数的最大值(包含嵌套结构体的对齐数)的整数倍,这里最大对齐数是 struct S
的 8
,因此,结构体 S2
的总大小为 32
字节。
编译运行上面程序,结果如下:
[root@localhost alianed]# ./aligned
结构体S的大小为:16
结构体S1的大小为:8
S2 结构体中:
a 的偏移量: 0
str 的偏移量: 4
s 的偏移量: 16
结构体S2的大小为:32
__attribute__((aligned(8)))
修饰嵌套结构体时,会强制该结构体在内存中按照 8
字节对齐,而不是使用默认的对齐方式。它的作用主要体现在以下几个方面:
对齐要求提高:嵌套的结构体被强制按照 8
字节对齐,即使其中的成员变量默认只需 4
字节或更小的对齐,编译器也会在嵌套结构体前插入填充字节,以确保其起始地址为 8
的倍数。这是为了满足硬件对齐要求或者性能优化的需要。
影响内存布局:由于对齐要求的提高,编译器在嵌套结构体前可能会插入额外的填充字节,导致结构体的内存布局发生变化。这会影响嵌套结构体的起始偏移量,并可能增加整个结构体的大小。
性能优化:某些硬件平台(如 64
位系统或 SIMD
指令集)对数据对齐有较高要求,合理的对齐可以提升数据访问的效率。通过 __attribute__((aligned(8)))
,可以确保结构体在这些平台上更高效地被访问。
内存浪费的权衡:强制对齐可能导致一些内存的浪费,特别是在对齐边界和结构体成员大小不匹配时。但这种浪费通常是为了换取更好的访问性能,因此在需要对齐优化时应合理使用。
__attribute__((aligned(alignment)))
是一种用于指定数据对齐方式的编译器属性,广泛应用于 C
和 C++
编程中。通过对齐属性,开发者可以显式地控制数据在内存中的布局,从而优化程序性能,尤其是在涉及高性能计算、嵌入式系统和硬件接口等场景下。合理的对齐可以减少内存访问延迟,提高缓存效率,并避免处理器因数据未对齐而导致的异常处理开销。然而,不当使用对齐属性可能导致不必要的内存浪费或破坏数据布局的一致性,因此在应用时需谨慎考虑对齐需求与实际内存使用之间的平衡。总之,__attribute__((aligned(alignment)))
是一个强大的工具,能够帮助开发者在特定场景下获得更好的性能表现。