字节对齐是C语言中的一个概念,但什么是字节对齐?对齐准则是什么?为什么要进行字节对齐呢?字节对齐会不会导致一些额定问题呢? 字节对齐对咱们编程有什么启示意义和考虑?
带着这四个疑问咱们聊聊字节对齐。

什么是字节对齐?

计算机内存巨细根本单位是字节(byte),理论上讲,能够从恣意地址拜访某种根本数据类型,可是实践上,计算机并非逐字节巨细读写内存,而是以2,4,或8的 倍数的字节块来读写内存,如此一来就会对根本数据类型的合法地址作出一些约束,即它的地址有必要是2,4或8的倍数。那么就要求各种数据类型依照必定的规矩在空间上排列,这便是对齐。

对齐准则是什么?

对齐的分类

Intel X86架构为例,对齐办法可分为:结构体对齐栈内存对齐位域对齐(位域本质上为结构体类型);
关于Intel X86架构渠道,每次分配内存应该是从4的整数倍地址开端分配,无论是对结构体变量还是简单类型的变量。

每个特定的渠道上的编译器都有自己的默许“对齐系数”(也叫对齐模数)。咱们能够通过预编译命令#pragma pack(n),n=1、2、4、8、16 来改动这一系数,其中的n便是要指定的“对齐系数”。咱们iOS编译器Xcode的对齐系数便是8。

字节对齐的问题首要便是针对结构体,所以咱们这儿首要讨论结构体对齐办法,对栈内存对齐位域对齐感兴趣同学能够自行查资料哈;

在C语言中,结构体是一种契合数据类型,其构成元素既能够是根本数据类型(如int、long、float等)的变量,也能够是一些复合数据类型(如数组、结构体、联合等)的数据单元。编译器为结构体的每个成员依照其天然边界(alignment)分配空间。各成员依照它们被声明的次序在内存中次序存储,第一个成员的地址和整个结构的地址相同。

结构体对齐准则

先来看四个重要的根本概念:

  1. 数据类型本身的对齐值:char型数据本身对齐值为1字节,short型数据为2字节,int/float型为4字节,double型为8字节。
  2. 结构体或类的本身对齐值:其成员中本身对齐值最大的那个值。
  3. 指定对齐值:#pragma pack (value)时的指定对齐值value
  4. 数据成员、结构体和类的有用对齐值:本身对齐值和指定对齐值中较小者,即有用对齐值=min{本身对齐值,当时指定的pack值}

基于上面这些值,就能够方便地讨论详细数据结构的成员和其本身的对齐办法。
上面的概念便于了解,结构体字节对齐的细节和详细编译器完结相关,一般满意下面三个准则:

  1. 结构体变量的首地址能够被其最宽根本类型成员的巨细所整除;
  2. 结构体每个成员相对结构体首地址的偏移量(offset)都是成员巨细的整数倍,如有需求编译器会在成员之间加上填充字节(internal adding);
  3. 结构体的总巨细为结构体最宽根本类型成员巨细的整数倍,如有需求编译器会在最末一个成员之后加上填充字节{trailing padding}。

关于以上规矩能够这么了解:

  • 第一条:编译器在给结构体拓荒空间时,首要找到结构体中最宽的根本数据类型,然后寻觅内存地址能被该根本数据类型所整除的方位,作为结构体的首地址。将这个最宽的根本数据类型的巨细作为对齐模数。
  • 第二条:为结构体的一个成员拓荒空间之前,编译器首要检查预拓荒空间的首地址相关于结构体首地址的偏移是否是本成员巨细的整数倍,若是,则寄存本成员,反之,则在本成员和上一个成员之间填充必定的字节,以达到整数倍的要求,也便是将预拓荒空间的首地址后移几个字节。
  • 第三条:结构体总巨细是包括填充字节,最终一个成员满意上面两条以外,还有必要满意第三条,否则就有必要在最终填充几个字节以达到本条要求。

结构体对齐示例

示例1:

界说结构体如下,已知32位机器上各数据类型的长度为:char为1字节、short为2字节、int为4字节、long为4字节、float为4字节、double为8字节。
那么sizeof(strcut A)值;sizeof(struct B)的值 分别是什么?

/// 32位,X86处理器,GCC编译器
struct A{
    int    a;
    char   b;
    short  c;
};
struct B{
    char   b;
    int    a;
    short  c;
};

成果是:sizeof(strcut A)值为8;sizeof(struct B)的值却是12。

假定B从地址空间0x0000开端寄存,且指定对齐值默许为4(4字节对齐)。成员变量b的本身对齐值是1,比默许指定对齐值4小,所以其有用对齐值为1,其寄存地址0x0000契合0x0000%1=0。成员变量a本身对齐值为4,所以有用对齐值也为4,只能寄存在开端地址为0x0004~0x0007四个接连的字节空间中,契合0x0004%4=0且紧靠第一个变量。变量c本身对齐值为 2,所以有用对齐值也是2,可寄存在0x0008~0x0009两个字节空间中,契合0x0008%2=0。所以从0x0000~0x0009寄存的都是B内容。

再看数据结构B的本身对齐值为其变量中最大对齐值(这儿是b)所以便是4,所以结构体的有用对齐值也是4。依据结构体圆整的要求, 0x0000~0x0009=10字节,(10+2)%4=0。所以0x0000A~0x000B也为结构体B所占用。故B从0x00000x000B 共有12个字节,sizeof(struct B)=12。

之所以编译器在后面弥补2个字节,是为了完结结构数组的存取功率。试想假如界说一个结构B的数组,那么第一个结构开端地址是0没有问题,可是第二个结构呢?依照数组的界说,数组中所有元素都紧挨着。假如咱们不把结构体巨细弥补为4的整数倍,那么下一个结构的开端地址将是0x0000A,这显然不能满意结构的地址对齐。因而要把结构体弥补成有用对齐巨细的整数倍。其实关于char/short/int/float/double等已有类型的本身对齐值也是基于数组考虑的,只是由于这些类型的长度已知,所以他们的本身对齐值也就已知。

示例2:

/// 32位,X86处理器,GCC编译器
#include<stdio.h> 
#include<stdint.h> 
struct test { 
    int a; 
    char b; 
    int c; 
    short d;
}; 
int main(int argc,char *argv) { 
    /*在32位和64位的机器上,size_t的巨细不同*/ 
    printf("the size of struct test is %zu\n",sizeof(struct test)); 
    return 0; 
}

编译成32位程序并运行(默许四字节天然对齐),能够看到,结构体test 的巨细为16字节,而不是11字节(a占4字节,b占1字节,c占4字节,d占2字节)

#64位机器上编译32位程序或许需求装置一个库
#sudo apt-get install gcc-multilib gcc -m32 -o testByteAlign testByteAlign.c 
#编译程序 chmod +x testByteAlign 
#赋履行权限 ./testByteAlign 
#运行 the size of struct test is 16

实践上,结构体test的成员在内存中或许是像下面这样散布的(数值为偏移量)。

未对齐时:

0~3 4 5~9 10~11
a b c d

对齐时:

0~3 4 5~7 8~11 12~13 14~15
a b 填充内容 c d 填充内容

示例3:

/* OFFSET宏界说可取得指定结构体某成员在结构体内部的偏移 */
#define OFFSET(st, field)     (size_t)&(((st*)0)->field)
typedef struct{
    char  a;
    short b;
    char  c;
    int   d;
    char  e[3];
}T_Test;
int main(void){
    printf("Size = %d\n  a-%d, b-%d, c-%d, d-%d\n  e[0]-%d, e[1]-%d, e[2]-%d\n",
           sizeof(T_Test), OFFSET(T_Test, a), OFFSET(T_Test, b),
           OFFSET(T_Test, c), OFFSET(T_Test, d), OFFSET(T_Test, e[0]),
           OFFSET(T_Test, e[1]),OFFSET(T_Test, e[2]));
    return 0;
}

输出:

Size = 16
    a-0, b-2, c-4, d-8
    e[0]-12, e[1]-13, e[2]-14

short b本身占用2个字节,依据上面准则2,需求在b和a之间填充1个字节。

char c占用1个字节,没问题。

int d本身占用4个字节,依据准则2,需求在d和c之间填充3个字节。

char e[3];本身占用3个字节,依据准则3,需求在其后弥补1个字节。

因而,sizeof(T_Test) = 1 + 1 + 2 + 1 + 3 + 4 + 3 + 1 = 16字节。

为什么要进行字节对齐呢?

其实无论数据是否对齐,大多计算机仍能够正常工作,上面示例2中个结构体test本来只需求11个字节的空间,而对齐后却占用了16字节,很明显浪费了空间,可为什么还要进行字节退旗呢?

其实首要原因是:为了进步内存体系功能 (空间换功能)

计算机每次读写一个字节块,例如,假定计算机总是从内存中取8个字节,假如一个double数据的地址对齐成8的倍数,那么一个内存操作就能够读或许写,可是假如这个double数据的地址没有对齐,数据就或许被放在两个8字节块中,那么咱们或许需求履行两次内存拜访,才干读写完结。显然在这样的状况下,是低效的。所以需求字节对齐来进步内存体系功能。

而且不同硬件渠道对存储空间的处理上存在很大的不同。某些渠道对特定类型的数据只能从特定地址开端存取,而不允许其在内存中恣意寄存。例如Motorola 68000 处理器不允许16位的字寄存在奇地址,否则会触发异常,因而在这种架构下编程有必要保证字节对齐。

因而,通过合理的内存对齐能够进步拜访功率。为使CPU能够对数据进行快速拜访,数据的开端地址应具有“对齐”特性。比方4字节数据的开端地址应位于4字节边界上,即开端地址能够被4整除。

此外,合理运用字节对齐还能够有用地节约存储空间。但要留意,在32位机中运用1字节或2字节对齐,反而会降低变量拜访速度。因而需求考虑处理器类型。还应考虑编译器的类型。在VC/C++和GNU GCC中都是默许是4字节对齐。

字节对齐会不会导致一些额定问题呢?

字节对齐当然能够提升内存体系功能,但一起也埋下了不少隐患:

  • 数据类型转换
  • 处理器间数据通讯

字节对齐留意点及考虑?

需求留意的

呈现对齐或许赋值问题咱们如何排查呢?

能够从一下几方面:

  1. 编译器的字节序巨细端设置;
  2. 处理器架构本身是否支撑非对齐拜访;
  3. 假如支撑看设置对齐与否,假如没有则看拜访时需求加某些特别的润饰来标志其特别拜访操作。

更改对齐办法

关于C语言,咱们能够修编译器的缺省字节对齐办法。
在缺省状况下,C编译器为每一个变量或是数据单元按其天然对界条件分配空间。一般地,能够通过下面的办法来改动缺省的对界条件:

  • 运用伪指令#pragma pack(n):C编译器将依照n个字节对齐;
  • 运用伪指令#pragma pack(): 撤销自界说字节对齐办法。
    另外,还有如下的一种办法(GCC特有语法):
  • __attribute((aligned (n))): 让所效果的结构成员对齐在n字节天然边界上。假如结构体中有成员的长度大于n,则依照最大成员的长度来对齐。
  • __attribute__ ((packed)): 撤销结构在编译过程中的优化对齐,依照实践占用字节数进行对齐。
#pragma pack(2)  //指定按2字节对齐
struct C{
    char  b;
    int   a;
    short c;
};
#pragma pack()   //撤销指定对齐,恢复缺省对齐

变量b本身对齐值为1,指定对齐值为2,所以有用对齐值为1,假定C从0x0000开端,则b寄存在0x0000,契合0x0000%1= 0;变量a本身对齐值为4,指定对齐值为2,所以有用对齐值为2,次序寄存在0x0002~0x0005四个接连字节中,契合0x0002%2=0。变量c的本身对齐值为2,所以有用对齐值为2,次序寄存在0x0006~0x0007中,契合 0x0006%2=0。所以从0x00000x00007共八字节寄存的是C的变量。C的本身对齐值为4,所以其有用对齐值为2。又8%2=0,C只占用0x0000~0x0007的八个字节。所以sizeof(struct C) = 8

留意,结构体对齐到的字节数并非彻底取决于当时指定的pack值,如下:

#pragma pack(8)
struct D{
    char  b;
    short a;
    char  c;
};
#pragma pack()

尽管#pragma pack(8),但依然依照两字节对齐,所以sizeof(struct D)的值为6。由于:对齐到的字节数 = min{当时指定的pack值,最大成员巨细}。

另外,GNU GCC编译器中按1字节对齐可写为以下形式:

#define GNUC_PACKED __attribute__((packed))
struct C{
    char  b;
    int   a;
    short c;
}GNUC_PACKED;

此时sizeof(struct C)的值为7。

开发中的考虑:

尽管字节对齐是由编译器来完结,可是咱们在日常编程中仍需重视字节对齐的优化问题;

空间存储

如下结构体test,其占用空间巨细为16字节,可是假如咱们换一种声明办法,调整变量的次序,从头运行程序,最终发现结构体test占用巨细为12字节

struct test {
    int a; 
    char b;
    short d; 
    int c; 
};

空间存储状况如下,b和d存储在了一个字节块中:

0~3 4 5 6~7 8~11
a b 填充内容 d c

也便是说,假如咱们在设计结构的时分,合理调整成员的方位,能够大大节约存储空间。可是需求在空间和可读性之间进行权衡。

跨渠道通讯

由于不同渠道对齐办法或许不同,如此一来,同样的结构在不同的渠道其巨细或许不同,在无意识的状况下,互相发送的数据或许呈现错乱,甚至引发严峻的问题。因而,为了不同处理器之间能够正确的处理消息,咱们有两种可选的处理办法。

  • 1字节对齐
  • 自己对结构进行字节填充

咱们能够运用伪指令#pragma pack(n)(n为字节对齐数)来使得结构间一字节对齐。 同样是前面的程序,假如在结构体test的前面加上伪指令,即如下:

#pragma pack(1) /*1字节对齐*/ 
struct test { 
    int a; 
    char b; 
    int c; 
    short d; 
}; 
#pragma pack()/*还原默许对齐*/

除了前面的1字节对齐,还能够进行人为的填充,即test结构体声明如下:

struct test {
    int a; 
    char b;
    char reserve[3]; 
    int c; 
    short d;
    char reserve1[2]; 
};

拜访功率高,但并不节约空间,一起扩展性不是很好,例如,当字节对齐有变化时,需求填充的字节数或许就会发生变化。

看下iOS中字节对齐

@interface XDPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) long height;
@property (nonatomic, copy) NSString *sex;
@property (nonatomic) char ch1;
@property (nonatomic) char ch2;
@end
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    XDPerson *p1 = [XDPerson alloc];
    p1.name = @"xiedong";
    p1.age = 18;
    p1.height = 180;
    p1.sex = @"男";
    p1.ch1 = 'a';
    p1.ch2 = 'b';
   NSLog(@"%lu - %lu",class_getInstanceSize([p1 class]),malloc_size((__bridge const void *)(p1)));
}
输出成果 40 - 48。
  • 对象请求的内存空间 <= 体系拓荒的内存空间。
  • 对象请求的内存空间是以8字节对齐办法。在objc源码里边是能够得到验证的。
  • 体系拓荒内存空间是以16字节对齐办法。在malloc源码里边segregated_size_to_fit()能够看到是以16字节对齐的。

运用lldb调试检查内存地址信息,

x/6xg p1意思代表 读取p1对象6段内存地址。

(lldb) x/6xg p1
0x600000ce0000: 0x00000001029570d0 0x0000001200006261
0x600000ce0010: 0x0000000102956098 0x00000000000000b4
0x600000ce0020: 0x00000001029560b8 0x0000000000000000
(lldb) po 0x00000001029570d0 & 0x0000000ffffffff8
XDPerson
(lldb) po 0x00000012
18
(lldb) po 0x62
98
(lldb) po 0x61
97
(lldb) po 0x0000000102956098
xiedong
(lldb) po 0x00000000000000b4
180
(lldb) po 0x00000001029560b8
男

发现OC里边程序员写的特点的次序并不是内存里边的次序,与结构体struct还是有必定的区别。其实这儿便是编译器给进行二进制重排产生的效果。

第一个内存地址是isa,是objc_object这个基类带的数据成员

总结

字节对齐的细节尽管编译器在做,可是咱们仍有重视,不然或许会在编程中遇到难以了解或处理的问题。
因而针对字节对齐,总结了以下处理主张:

  • 结构体成员合理安排方位,以节约空间
  • 跨渠道数据结构可考虑1字节对齐,节约空间但影响拜访功率
  • 跨渠道数据结构人为进行字节填充,进步拜访功率但不节约空间
  • 本地数据采用默许对齐,以进步拜访功率
  • 32位与64位默许对齐数不一样 ,分别是4字节和8字节对齐

参阅:

  • 字节对齐,看这篇就懂了 – 腾讯云开发者社区-腾讯云
  • C语言字节对齐问题详解 – clover_toeic – 博客园
  • iOS 底层探索篇 —— 内存字节对齐分析 –