byte alignment

内存空间按照字节划分,理论上可以从任何起始地址访问任意类型的变量。但实际中在访问特定类型变量时经常在特定的内存地址访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是顺序一个接一个地存放,这就是对齐。

32位编译模式下,默认以4字节对齐;在64位编译模式下,默认以8字节对齐。

先来看一个例子: 假设32位处理器

# include <stdio.h>

typedef struct {
    short b;
    char a;
} A;

void main() {
    A t;
    printf("size = %d\n", sizeof(A));
    printf("b-->%p, a-->%p\n",
        &t.b ,
        &t.a 
    );
}

sizeof计算结构体类型A的大小,按我们正常的逻辑计算可以知道 2(b所占字节) + 1(a所占字节) = 3 字节, 但是运行结果却是4字节, 如下:

[root@iz2zecj7a5r32f2axsctb9z extern]# ./byteAlign 
size = 4
b-->0x7ffd3b1082a0, a-->0x7ffd3b1082a2

为什么不一样呢,这就是字节对齐产生的结果,带着为什么、怎么处理的疑问往下看。

为什么要有字节对齐

对于我们来说,数据在内存中存储粒度为Byte,那么cpu一次性的访问粒度一般为偶数倍字节

  • CPU与内存
    不是所有的硬件架构都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取特定类型的数据,否则抛出硬件异常。所以需要指定一定的规则达成cpu与内存的通信。

    • CPU访问粒度指出CPU只能访问对齐地址上的固定长度的数据。 以四字节对齐为例,就是只能访问 0x0 - 0x3,0x4 - 0x7, 0x8 - 0xc 这样的(闭)区间,不能跨区间访问。如果真正需要访问的数据并没有占据那个区间的全部字节范围,还有另外的信号线来指出具体操作哪几个字节,类似于掩码的作用。
  • 以空间换时间提高CPU的读取效率。

    • 假设一个处理器总是从存储器中取出8个字节,则地址必须为8的倍数。如果我们能保证将所有的double类型数据的地址对齐成8的倍数,那么就可以用一个存储器操作来读或者写值了。否则,我们可能需要执行两次存储器访问,因为对象可能被分放在两个8字节存储块中
    • 假设一个整型变量的地址不是自然对齐,比如为0x00000002,则CPU如果取它的值的话需要访问两次内存,第一次取从0x00000002-0x00000003的一个short,第二次取从0x00000004-0x00000005的一个short然后组合得到所要的数据;如果变量在0x00000003地址上的话则要访问三次内存,第一次为char,第二次为short,第三次为char,然后组合得到整型数据。而如果变量在自然对齐位置上,则只要一次就可以取出数据。

自然对齐: 如果一个变量的内存地址正好位于它长度整数倍,他就被称做自然对齐。比如在32位cpu下,假设一个整型变量的地址为0x00000004,那它就是自然对齐的。

什么时候需要字节对齐

在设计不同CPU下的通信协议时,或者编写硬件驱动程序时寄存器的结构这两个地方都需要按一字节对齐。即使看起来本来就自然对齐的也要使其对齐,以免不同的编译器生成的代码不一样.

对齐标准

不同系统下有不同的对齐标准

DataType ILP32 ILP64 LP64 LLP64
char 8 8 8 8
short 16 16 16 16
int 32 64 32 32
long 32 64 64 32
long long 64 64 64 64
pointer 32 64 64 64

绝大部分64位的unix、linux 都是使用的LP64模型; 32位linux系统是ILP32模型;64位的window使用的是LLP64( long long and point 64)模型。

如何处理字节对齐

编译器依照的对齐规则:

  • 数据类型自身的对齐值:为指定平台上基本类型的长度。对于char型数据,其自身对齐值为1,对于short型为2,对于int,float,double类型,其自身对齐值为4,单位字节。
  • 结构体或者类的自身对齐值:其成员中自身对齐值最大的那个值。
  • 指定对齐值:#pragma pack (value)时的指定对齐值value。
  • 数据成员、结构体和类的有效对齐值:自身对齐值和指定对齐值中小的那个值。

对于标准数据类型,它的地址只要是它的长度的整数倍就行了,而非标准数据类型按下面的原则对齐:

  • 数组 :按照基本数据类型对齐,第一个对齐了后面的自然也就对齐了。
  • 联合 :按其包含的长度最大的数据类型对齐。
  • 结构体: 结构体中每个数据类型都要对齐

与对齐相关的四个值:

  • 指定对齐值:代码中指定的对齐值,记为packLen;
  • 默认对齐值:结构体中每个数据成员及结构体本身都有默认对齐值,记为defaultLen;
  • 成员偏移量:即相对于结构体起始位置的长度,记为offset;
  • 成员长度:结构体中每个数据成员的长度(注结构体成员为补齐之后的长度),记为memberLen

对齐规则:

  • 对齐规则: offset % vaildLen = 0,其中vaildLen为有效对齐值vaildLen = min(packLen, defaultLen);
  • 填充规则: 如成员变量不遵守对齐规则,则需要对其补齐;在其前面填充一些字节保证该成员对齐。需填充的字节数记为pad

简单来说:数据的起始地址必须是该数据所占字节的整数倍, 同时所占的整体大小必须是对齐值最大值宽度的整数倍

实例讲解

实例1

#include <stdio.h>

typedef struct {
        char a;
        short b;
        int c;
        long d;
        char  e[3];
} test_t;

int main(){
    test_t t;
    char *p = t.e;
    printf("Size = %d\n  a-%p, b-%p, c-%p, d-%p\n  e[0]-%p, e[1]-%p, e[2]-%p\n",
            sizeof(test_t), &t.a, &t.b,
            &t.c, &t.d, p,
            p+1,p+2);
     return 0;
}

在64位centos上运行结果如下:

[root@iz2zecj7a5r32f2axsctb9z extern]# ./byteAlign1
Size = 24
  a-0x7ffc75a4c690, b-0x7ffc75a4c692, c-0x7ffc75a4c694, d-0x7ffc75a4c698
  e[0]-0x7ffc75a4c6a0, e[1]-0x7ffc75a4c6a1, e[2]-0x7ffc75a4c6a2

分析得到结构体test_t的内存布局如图所示:
布局

根据对齐规则,逐个分析

  • char a 的自身对齐值为1,所以放在offset=0的位置
  • short b 的自身对齐值为2, 所以在a位置后面有一个字节的padding
  • int c 的自身对齐值为4, 所以正好从offset=4的位置开始
  • long d的自身对齐值为8, 所以正好从offset=8的位置开始
  • char e[3] 的自身对齐值为1, 所在在d的后面直接分配3个字节
  • 整体的大小应该为成员自身对齐值最大的那个值的整数倍,所以应该为8的整数倍,所以后面padding了5个字节
  • 因此整体所占字节数为 24字节

实例2

struct test2 {
    int a;
    long b;
    char c;
};

内存布局如下:
布局

实例3

struct test3 {
    char c;
    int a;
    long b;
};

内存布局如下:
内存布局

如何控制内存对齐方式

  • 编译注解机制 pragma

#pragma pack(1) // 用编译注解强制按照1Byte方式对齐

#pragma pack() // 取消指定,恢复默认方式

注意: 使用pragma机制更改对齐宽度时,编译器会比较“用户指定宽度”和“成员最大基本类型宽度”,并选取较小的宽度对齐

  • attribute 机制
    typedef struct _s2_ {
      char ch2;
      short s;
      char ch;
    }__attribute__((aligned(4)))  s2_t 
    使用GNU的attribute机制使用4Byte对齐,注意,如果结构体内部有基本类型大于aligned指定大小的话,则按照更长的对齐方式。
typedef  struct  _s3_ {
    char ch2;
    int i;
    char ch;
}__attribute__((packed)) s3_t

取消优化对齐,按照实际大小分配,无对齐

注意: 使用attribute机制更改对齐宽度时,编译器会比较“用户指定宽度”和“成员最大基本类型宽度”,并选取较大的宽度对齐。

实例

#include <stdio.h>
#pragma pack(2)
typedef struct  {
    char b;
    int a;
    short c;
} C;
#pragma pack()

int main(){
    printf("size of type A = %d\n", sizeof(C));
    return 0;
}

运行结果为:

[root@iz2zecj7a5r32f2axsctb9z extern]# ./byteAlign2
size of type A = 8

分析: 第一个变量b的自身对齐值为1,指定对齐值为2,所以,其有效对齐值为1,假设C从0x0000开始,那么b存放在0x0000,符合0x0000%1=0;第二个变量,自身对齐值为4,指定对齐值为2,所以有效对齐值为2,所以顺序存放在0x0002、0x0003、0x0004、0x0005四个连续字节中,符合0x0002%2=0。第三个变量c的自身对齐值为2,所以有效对齐值为2,顺序存放在0x0006、0x0007中,符合0x0006%2=0。所以从0x0000到0x00007共八字节存放的是C的变量。又C的自身对齐值为4,所以C的有效对齐值为2。又8%2=0,C只占用0x0000到0x0007的八个字节。所以sizeof(struct C)=8.

结构体嵌套

对齐思想为: 深度优先填充

#include <stdio.h>

struct test1 {
    int a;
    long b;
};
struct test2 {
    char a;
    struct test1 b;
    int c;
};

int main(){
    printf("size = %d\n", sizeof(struct test2));
}

运行结果为:

[root@iz2zecj7a5r32f2axsctb9z extern]# ./byteAlign3
size = 32

分析得到test1的布局:
test1布局
分析得到test2布局:
test2布局

文档更新时间: 2021-01-31 15:21   作者:周国强