C语言结构体内存对齐(#prama / __attribute__)

本文测试机器为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的使用

二、为什么需要内存对齐

至于为什么需要内存对齐,可以参考这篇文章。

三、#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__

通过__attribute__((aligned(x)))也可以进行内存对齐,其用法较复杂,可用于cache对齐,减少cache miss。与#pragma pack的区别如下。

1、#pragma pack(x) 影响整个结构体的起始地址、结构体大小,以及所有成员的对齐方式,但无法直接作用于单个成员;
2、__attribute__((aligned(x))) 仅能影响结构体的起始地址及结构体大小(放在结构体之外),或者影响单个成员的对齐方式(放在结构体之内修饰单个成员)。

  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的大小,则不起作用。

3、特殊情况__attribute__((packed))修饰时,该成员按紧凑格式对齐,举例如下:
#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 ,相关测试可参考这篇文章。

你可能感兴趣的:(C/C++)