引言
在 C
语言中,内存对齐(Memory Alignment
)是影响程序性能的重要因素之一。对齐不仅影响访问速度,还可能影响程序的正确性,特别是在硬件架构要求严格对齐的系统上。GNU C
编译器(GCC
)提供了一种方便的机制来控制内存对齐:__attribute__((aligned(alignment)))
。通过这个属性,我们可以控制变量、结构体或数组的对齐方式,以便优化性能或满足特殊需求。
本篇文章将深入介绍 __attribute__((aligned(alignment)))
的工作原理,并通过代码实例来演示它的应用场景。
工作原理
什么是内存对齐
内存对齐是指变量的地址满足特定的边界要求。通常,编译器会根据目标平台的要求自动分配对齐。例如,在 32
位系统上,int
类型的变量一般按 4
字节对齐,而 double
可能按 8
字节对齐。
通过对齐,可以保证 CPU
在读取或写入数据时,能够在最少的指令周期内完成访问。如果变量没有对齐到合适的边界,可能需要额外的指令进行内存操作,导致性能下降。
__attribute__((aligned(alignment)))的作用
__attribute__((aligned(alignment)))
是 GCC
提供的一个属性,用于手动设置变量或结构体的对齐方式。它允许你指定对齐边界,单位是字节。该属性可以在变量声明或类型定义时使用。它的作用包括:
-
提高性能:通过为特定的变量设置合适的对齐方式,提升内存访问的效率。 -
满足硬件要求:某些硬件或设备驱动程序可能要求特定的对齐方式,通过这个属性可以满足这些要求。 -
处理 SIMD 指令:某些高级指令集(如 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
,编译器会选择合适的对齐值,通常是变量类型的默认对齐方式。
代码演示
1. 修饰变量
源文件 test.c
:
#include <stdio.h>
#include <stddef.h>
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 的偏移量: %zun", offsetof(struct NoAlign, a));
printf(" b 的偏移量: %zun", offsetof(struct NoAlign, b));
printf(" NoAlign 的大小: %zun", sizeof(struct NoAlign));
printf("nWithAlign 结构体中:n");
printf(" a 的偏移量: %zun", offsetof(struct WithAlign, a));
printf(" b 的偏移量: %zun", offsetof(struct WithAlign, b));
printf(" WithAlign 的大小: %zun", 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))
后,内存布局发生了变化,尤其是当对齐要求大于成员类型的默认对齐时,填充字节的数量会显著增加。
2. 修饰结构体
源文件 test_struct.c
:
#include <stdio.h>
#include <stddef.h>
struct S2 {
short b[3]; // 一个包含 3 个 short 元素的数组
int a; // 一个 int 类型变量
char c; // 一个 char 类型变量
} __attribute__((aligned(8))); // 按照 8 字节对齐
int main() {
// 打印结构体成员的偏移量以及整个结构体的大小
printf("S2 结构体中:n");
printf(" b 的偏移量: %zun", offsetof(struct S2, b));
printf(" a 的偏移量: %zun", offsetof(struct S2, a));
printf(" c 的偏移量: %zun", offsetof(struct S2, c));
printf(" S2 的大小: %zun", 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 的偏移量: %zun", offsetof(struct S2, b));
printf(" a 的偏移量: %zun", offsetof(struct S2, a));
printf(" c 的偏移量: %zun", offsetof(struct S2, c));
printf(" S2 的大小: %zun", 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
字节对齐,编译器可能会在必要时在成员之间或末尾插入填充字节。
3. 修饰嵌套结构体
源文件 aligned.c
:
#include<stdio.h>
#include<stddef.h>
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 的偏移量: %zun", offsetof(struct S2, a));
printf("str 的偏移量: %zun", offsetof(struct S2, str));
printf("s 的偏移量: %zun", 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)))
是一个强大的工具,能够帮助开发者在特定场景下获得更好的性能表现。
原文始发于微信公众号(Linux二进制):gcc __attribute__((aligned(alignment)))解析
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/301982.html