gcc __attribute__((aligned(alignment)))解析

引言

在 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 上默认对齐数是 8gcc 没有默认对齐数,对齐数就是变量本身的大小。

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 cchar 只占用 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 字节对齐。具体而言:

  1. 结构体本身的对齐:结构体的起始地址会强制按照 8 字节对齐。这意味着当该结构体实例在内存中分配时,它的起始地址会是 8 的倍数。

  2. 结构体内部成员的对齐:每个成员依然根据其自身的数据类型以及默认的对齐要求进行对齐,但会考虑 __attribute__((aligned(8))) 对结构体整体对齐的影响。一般情况下,编译器会根据成员的类型和自身的对齐要求来安排它们的内存位置,插入必要的填充字节。

  3. 结构体大小的对齐:最终,结构体的大小会被调整为 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))) 设置的强制对齐。上述代码解析如下:

  • 结构体 Sint a 占用 4 字节,char b[5] 占用 5 字节,int c 占用 4 字节。结构体 S 的总大小为 16 字节(并不是 13 字节,因为编译器为 int c 对齐填充了 3 字节的空隙,使结构体大小对齐到 4 的倍数)。

  • 结构体 S1int a 占用 4 字节,char b 占用 1 字节。这里的总大小是 8 字节(而不是 5 字节),因为 char b 之后,编译器会插入 3 字节的填充,以确保结构体大小为最大对齐数 int 的倍数(即 4 字节的倍数),以提高对齐效率。

  • 结构体 S2int a 占用 4 字节,从偏移量 0 开始;char str[5] 紧接在 a 后面,占用 5 字节,偏移量为 4。由于 str 的结束地址是 9(从偏移量 4 开始), struct S 的 __attribute__((aligned(8))) 指定了 8 字节对齐,因此它的偏移量为 16,并且它的大小仍然为 16 字节。为了让 struct S 的地址对齐到 168 的倍数),编译器插入了 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 字节对齐,而不是使用默认的对齐方式。它的作用主要体现在以下几个方面:

  1. 对齐要求提高:嵌套的结构体被强制按照 8 字节对齐,即使其中的成员变量默认只需 4 字节或更小的对齐,编译器也会在嵌套结构体前插入填充字节,以确保其起始地址为 8 的倍数。这是为了满足硬件对齐要求或者性能优化的需要。

  2. 影响内存布局:由于对齐要求的提高,编译器在嵌套结构体前可能会插入额外的填充字节,导致结构体的内存布局发生变化。这会影响嵌套结构体的起始偏移量,并可能增加整个结构体的大小。

  3. 性能优化:某些硬件平台(如 64 位系统或 SIMD 指令集)对数据对齐有较高要求,合理的对齐可以提升数据访问的效率。通过 __attribute__((aligned(8))),可以确保结构体在这些平台上更高效地被访问。

  4. 内存浪费的权衡:强制对齐可能导致一些内存的浪费,特别是在对齐边界和结构体成员大小不匹配时。但这种浪费通常是为了换取更好的访问性能,因此在需要对齐优化时应合理使用。

总结

__attribute__((aligned(alignment))) 是一种用于指定数据对齐方式的编译器属性,广泛应用于 C 和 C++ 编程中。通过对齐属性,开发者可以显式地控制数据在内存中的布局,从而优化程序性能,尤其是在涉及高性能计算、嵌入式系统和硬件接口等场景下。合理的对齐可以减少内存访问延迟,提高缓存效率,并避免处理器因数据未对齐而导致的异常处理开销。然而,不当使用对齐属性可能导致不必要的内存浪费或破坏数据布局的一致性,因此在应用时需谨慎考虑对齐需求与实际内存使用之间的平衡。总之,__attribute__((aligned(alignment))) 是一个强大的工具,能够帮助开发者在特定场景下获得更好的性能表现。


原文始发于微信公众号(Linux二进制):gcc __attribute__((aligned(alignment)))解析

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/301982.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!