本文测试机器为X86_64架构,系统为ubuntu-16.04 LTS,gcc version 7.4.0
首先,通过一个例子来介绍什么是内存对齐。
#include
int main( )
{
struct data
{
float a;
char b;
int c;
};
struct data e ;
printf ( "%d\n", sizeof ( e ) ) ;
printf ( "%u %u %u\n", &e.a, &e.b, &e.c) ;
return 0 ;
}
输出为:
12
3495431596 3495431600 3495431604
主观上我们认为结构体大小是9字节,此处由于内存对齐,gcc会填充若干字节,填充规则如下:
1、结构体的起始地址按最大成员的大小进行对齐
2、每个成员按其大小进行对齐
3、结构体的大小必须是最大成员的整数倍
该实例中存在三个成员,最大成员占四个字节,所以结构体起始地址(成员a的地址)为4的倍数3495431596;由于成员a占四个字节,且成员b按1对齐,所以b的起始地址为3495431600;由于成员b只占一个字节,且成员c按4对齐,所以在b之后填充三个字节,然后再存储c。
事实上内存地址的对齐还受到 #pragma pack 的影响,下面会介绍#pragma pack的使用
至于为什么需要内存对齐,可以参考这篇文章。
programmer可以通过#pragma pack来控制内存对齐的方式,默认情况下#pragma pack按8字节对齐。
#pragma pack(x)会作用于整个结构体(所有成员):
1、将结构体最大成员的大小与#pragma pack的参数 x 比较,取较小值 t,结构体的起始地址为 t 的倍数,同时结构体的大小必须是 t 的倍数。
2、结构体每个成员将自身大小与 x 进行比较,该成员按照较小值进行对齐。
举个例子如下:
#include
int main( )
{
#pragma pack(4)
struct data
{
char a;
long long b;
char c;
};
struct data e ;
printf ( "%d\n", sizeof ( e ) ) ;
printf ( "%u %u %u\n", &e.a, &e.b, &e.c) ;
return 0;
}
结果如下:
16
3978367200 3978367204 3978367212
可以看到结构体起始地址按 4 对齐;成员a之后填充三个字节,使得成员b按 4 对齐;由于整个结构体的大小必须是4的倍数,所以成员c之后需要填充 3 个字节。
通过__attribute__((aligned(x)))也可以进行内存对齐,其用法较复杂,可用于cache对齐,减少cache miss。与#pragma pack的区别如下。
a、若__attribute__((aligned(x)))位于结构体之外:将 x 与最大成员的大小进行比较,得到较大值 t ,则结构体的起始地址必须是 t 的倍数,且结构体的大小也必须是 t 的倍数
b、若__attribute__((aligned(x)))位于结构体之内,修饰某个成员:将该成员的大小与 x 进行比较,得到较大值 t ,则该成员按 t 对齐。同时,将 t 与其他成员的大小进行比较,得到最大值 m,则结构体的起始地址为m的整数倍,且结构体的大小为 m 的整倍数。
修饰整个结构体,举例如下:
#include
int main( )
{
struct data
{
char a;
long long b;
char c;
} __attribute__((aligned(128))) ;
struct data e ;
printf ( "%d\n", sizeof ( e ) ) ;
printf ( "%u %u %u\n", &e.a, &e.b, &e.c) ;
return 0 ;
}
结果如下:
128
2244647168 2244647176 2244647184
内部对齐方式按默认方式对齐,结构体起始地址为128的整数倍,结构体大小也是128的整数倍
若将128改成4,__attribute__不再起作用,举例如下:
#include
int main( )
{
struct data
{
char a;
long long;
char c;
} __attribute__((aligned(4))) ;
struct data e ;
printf ( "%d\n", sizeof ( e ) ) ;
printf ( "%u %u %u\n", &e.a, &e.b, &e.c) ;
return 0 ;
}
结果如下:
24
1025053136 1025053144 1025053152
修饰单个成员变量,举例如下:
#include
int main( )
{
struct data
{
char a;
long long b __attribute__( ( aligned ( 128 ) ) ) ;
char c;
};
struct data e ;
printf ( "%d\n", sizeof ( e ) ) ;
printf ( "%u %u %u\n", &e.a, &e.b, &e.c) ;
return 0 ;
}
结果如下:
256
8371456 8371584 8371592
结构体起始地址按128对齐,成员b按128对齐,结构体大小为128整数倍。
修饰单个成员变量,举例如下:
#include
int main( )
{
struct data
{
char a;
long long b __attribute__( ( aligned ( 4 ) ) ) ;
char c
};
struct data e ;
printf ( "%d\n", sizeof ( e ) ) ;
printf ( "%u %u %u\n", &e.a, &e.b, &e.c) ;
return 0 ;
}
结果如下:
24
3466800 3466808 3466816
由于__attribute__对齐参数小于成员b的大小,则不起作用。
#include
int main( )
{
struct data
{
char a;
long long b;
char ;
} __attribute__((packed)) ;
struct data e ;
printf ( "%d\n", sizeof ( e ) ) ;
printf ( "%u %u %u\n", &e.a, &e.b, &e.c) ;
return 0 ;
}
结果如下:
10
2272583262 2272583263 2272583271
#include
int main( )
{
struct data
{
char a;
long long b __attribute__( ( packed ) ) ;
short c;
};
struct data e ;
printf ( "%d\n", sizeof ( e ) ) ;
printf ( "%u %u %u\n", &e.a, &e.b, &e.c) ;
return 0 ;
}
结果如下;
12
2717918348 2717918349 2717918358
1、由于#pragma pack(x)一般默认配置为 8,可以通过调整结构体成员的顺序来降低内存的使用。
2、需要改变默认对齐方式时,可以通过#pragma pack(x) 来改变,一般默认配置为 8。
3、__attribute__((aligned(x)))可以用来进行cache对齐,降低cache miss,如下所示:
假设cache line大小为64字节,我们定义一个结构体数组,保证每个元素都位于不同的cache line,定义如下:
struct Item{
int a;
short b;
char c;
}__attribute__((aligned(64));
struct Item array[10];
数组的每个元素位于单独的cache line,多线程访问(修改)数组的不同元素时可降低cache miss ,相关测试可参考这篇文章。