关于内存对齐,看我

先来看一下这个例子:

// 64位系统
#include<stdio.h>

struct{
    int  a;
    char b;
}s;

int main ()
{
    printf ("%d\n",sizeof(s);  
    return 0;
}

理论上,64位系统下,int占 4个byte,char占 1个byte,那么将它们放到一个结构体中应该占 4+1 = 5byte;但是实际上,通过运行程序得到的结果是 8byte,这就是内存对齐所导致的。

注:本文讨论的内容均是在64位系统下。

大纲

  • 什么是内存对齐

  • 为什么要内存对齐

  • 内存对齐规则

一、什么是内存对齐

计算机中内存空间是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是:在访问特定类型变量的时候通常在特定的内存地址访问,这就需要对这些数据在内存中存放的位置有限制,各种类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。

内存对齐是编译器的管辖范围。表现为:编译器为程序中的每个“数据单元”安排在适当的位置上。

二、为什么要内存对齐

为了解释这个问题,我们先要了解一下处理器是如何读取内存的?

我们如果把内存看做是简单的字节数组,比如在C语言中,char *就可表示一块内存。那么或许我们会认为,它的内存读取方式可以按照1byte顺序读取,如下图。

howProgrammersSeeMemory

然而,尽管内存是以字节为单位,但是大部分处理器并不是按字节块来存取内存的,这取决于数据类型和处理器的设置;它一般会以双字节,四字节,8字节,16字节甚至32字节的来存取内存,我们将上述这些存取单位称为内存存取粒度.

howProcessorsSeeMemory

现在我们知道,计算机的处理器是以一定大小的块来进行读取的,这作为我们的前提条件,那么为了解释为什么要内存对齐?我们不妨先看一看不对齐的情况会出现什么问题?

对齐跟数据在内存中的位置有关。如果一个变量的内存地址刚好位于它本身长度的整数倍,他就被称做自然对齐。例如一个整型变量(占4字节)的地址为0x00000016,那它就是自然对齐的。

现在假设一个整型变量(4字节)不是自然对齐的,它的起始地址落在0x00000002(图中蓝色区域),处理器想要访问它的值,按照4字节的块进行读取,从图中的0x0起读,读取4字节大小,读到0x3

这样的一次读取之后,我们并不能取到我们要访问的整型数据,紧接着处理器会继续再往下读,偏移4个字节,从0x4开始,读到0x7

到这里,处理器才能读取到了我们需要访问的内存数据,当然这中间还存在剔除与合并的过程。

所以,在此例中,当整型变量起始地址落在0x2时(不对齐),处理器需要两次读取才能取到我们要访问的内容。

那如果是对齐的呢?

显然,如果是对齐的,对于本例,仅需读取1次,我们便可以读取到目标数据。

可见,对齐与否会影响到我们的读取效率。

同时:

各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取,而不是内存中任意地址都是可以读取的。

比如有些架构的CPU在访问一个没有进行对齐的变量的时候会发生错误,那么在这种架构下编程必须保证字节对齐.其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台要求对数据存放进行对齐,会在存取效率上带来损失。

也正是由于只能在特定的地址处读取数据,所以在访问一些数据时,对于访问未对齐的内存,处理器可能需要进行多次访问;而对于对齐的内存,只需要访问一次就可以。

这就是为什么要内存对齐的原因。

内存对齐不仅便于CPU快速访问,同时合理的利用字节对齐可以有效地节省存储空间。

我们可以同时对比一下,不同内存存取粒度对同一任务的不同影响。

设定一个相同的任务:分别从Address0 和 Address1 中从地址0读取4个字节到处理器的寄存器中。

  • 先看单字节粒度情况
单字节内存存取粒度

两图中左侧表示内存,右侧表示寄存器,中间的箭头表示读取的过程。因为是单字节存取粒度,读取内存是按照1个字节进行访问的,所以对于Address0 来讲,要想从0的位置读取4个字节,需要读取4次,对于 Address1 也是一样的。即使他不是内存对齐的也无妨。

  • 再看双字节粒度情况
双字节内存存取粒度

从Address0 读取4个字节,相比于存取粒度为1字节的处理器,存取次数变成了一半,只需读取2次。由于每个内存访问都需要固定的开销,因此最小化访问次数确实可以提高性能。同时Address0 是内存对齐的(数据的起始位置落在0的位置上),所以第一次读取地址01,第二次读取地址23即可取到目标数据。

但是,从Address1 读取时。由于该地址未均匀地落在处理器的内存访问边界上(Address1 中寄存器黑框区域是该数据的内存地址区域,起始位置为1,不是对齐的),该处理器去取数据时,要先从0地址开始读取第一个2字节块(01),剔除不想要的字节(0地址),然后从地址2开始读取下一个2字节块(23),再从地址4开始读取下一个2字节块(45),剔除不想要的字节(地址5)。这样读取3次之后将最后留下的3块数据合并放入寄存器,才能取到目标数据。

  • 四字节粒度情况呢?
四字节内存存取粒度

具有四字节粒度的处理器从Address0 读取时,可以一次读取地址0123 就从对齐的地址中提取四个字节。

然而从Address1 读取时,因为是不对齐的,读取地址0123,剔除0地址,继而读取地址4567,剔除地址5、地址6、地址7 ,这样读取2次后将留下的2块数据合并放入寄存器,取到目标数据。

从双字节粒度和四字节粒度中可见,对于没有对齐的内存,需要做更多次的读取与剔除和合并的过程。这显然是降低效率的。

同时我们还需要注意到一点:对于对齐的内存,不同的存取粒度也会影响到存取效率。粒度小则存取次数多,粒度大则浪费空间。所以在每个特定平台上的编译器都有自己的默认存取粒度。

了解了内存对齐以及原因,我们继续看一下内存对齐的原则是什么。

三、内存对齐规则

对于标准数据类型

它的地址只要是它的长度的整数倍就行了

对于结构体

在结构体中,编译器为结构体的每个成员按其自然边界(alignment)分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构体的地址相同。具体规则如下:

  • 1.第一个成员在结构体变量偏移量为0 的地址处,也就是第一个成员必须从头开始。

  • 2.以后每个成员相对于结构体首地址的 offset 都是该成员大小的整数倍,如有需要编译器会在成员之间加上填充字节。

  • 3.结构体的总大小为 最大对齐数的整数倍(每个成员变量都有自己的对齐数),如有需要编译器会在最末一个成员之后加上填充字节。

  • 4.如果嵌套结构体,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(包含嵌套结构体的对齐数)的整数倍。

搞个例子尝尝

示例1:

struct test_t {
  int   a;
  long  b;
  short c;
};

第一个成员为int类型,占4字节,内存分布 00 01 02 03 ;用红色表示

从结构体的首地址开始将a写入内存

第二个成员为long类型,占8字节,此时内存的的偏移量04不是8的整数倍,所以要填充字节(绿色表示填充字节),到0x7的位置,然后将long类型的数据b写入内存(黄色)

填充4个字节并且将b类型写入内存

第三个成员是short类型,占2个字节,此时内存的偏移量16是short类型所占字节数的整数倍,所以直接写入内存(用蓝色表示)

将c写入内存

至此,结构体内的数据数据成员已对齐,但是当前结构体的总大小为18,不满足规则3,所以需在最末的成员后面填充6个字节,使其总大小为24。

填充6个字节以满足规则3

所以 此结构体的所占内存大小为24。

如果将结构体中的short类型与long类型换一下,

struct test_t {
  int   a;
  short b;
  long  c;
};

会是一个什么结果?我们依然用红色表示int类型,蓝色表示short类型,黄色表示long类型,绿色表示填充字节。

更换示例中short与long类型的位置后的结果

结果为16。可见,对于成员相同的结构体,如果改变成员的顺序,对于结构体所占空间的大小是会产生影响的,所以,我们不但要了解内存对齐,还是正确的利用内存对齐。

对于结构体嵌套结构体,在规则4中已经给出,这里的图就不再画了,聪明的你看到这里一定可以得到正确的答案。

我们再来回顾上面的对齐规则:

各成员变量存放的起始地址, 相对于结构的起始地址的偏移量 ,必须为该变量的类型所占用的字节数的倍数;

各成员变量在存放的时候根据在结构中出现的顺序依次申请空间, 同时按照上面的对齐方式调整位置, 空缺的字节自动填充

同时为了确保结构的大小为结构的字节边界数(即该结构中占用最大的空间的类型的字节数)的倍数,所以在为最后一个成员变量申请空间后 还会根据需要自动填充空缺的字节

这就是内存对齐的整体规则,但仍需注意的是:

在不同架构的处理器下,我们运行同一个示例得到的结果可能不同,甚至不同的编译器配置,也会影响这个结果,我们需要了解影响内存对齐规则的因素有哪些?

影响内存对齐结果的因素 ?

1.#pragma pack(n)

每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。程序员可以通过预编译命令#pragma pack(n),n=1,2,4,8,16来改变这一系数,其中的n就是你要指定的“对齐系数”。这里规定的是上界,只影响对齐单元大于n的成员,对于对齐字节不大于n的成员没有影响。

可以认为处理器一次性可以从内存中读/写n个字节。对于大小小于n的成员,按照自己的对齐条件对齐,因为不论怎么放都可以一次性取出。对于对齐条件大于n个字节的成员,成员按照自身的对齐条件对齐和按照n字节对齐需要相同的读取次数,但按照n字节对齐节省空间。

通过预编译命令#pragma pack()取消自定义字节对齐方式。

也可以写成:

#pragma pack(push,n)

#pragma pack(pop)

2.__attribute__((aligned (n)))

__attribute__((aligned (n))),让所作用的结构成员对齐在n字节自然边界上。如果结构中有成员的长度大于n,则按照最大成员的长度来对齐。

__attribute__((packed)),取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐。

需要注意的是:内存对齐的 对齐数 取决于 对齐系数 和 成员的字节数 两者之中的较小值。

举例说明:

struct test
{
char x1;
short x2;
float x3;
char x4;
}

默认情况下,结构的第一个成员x1,其偏移地址为0,占据了第1个字节。第二个成员x2为short类型,其起始地址必须2字节对界,因此,编译器在x2和x1之间填充了一个空字节。结构的第三个成员x3和第四个成员x4恰好落在其自然边界地址上,在它们前面不需要额外的填充字节。在test结构中,成员x3要求4字节对齐,是该结构所有成员中要求的最大边界单元,因而test结构的自然对齐条件为4字节,编译器在成员x4后面填充了3个空字节。整个结构所占据空间为12字节。

当使用:#pragma pack(1) //让编译器对这个结构作1字节对齐

#pragma pack(1) //让编译器对这个结构作1字节对齐
struct test
{
char x1;
short x2;
float x3;
char x4;
};
#pragma pack() //取消1字节对齐,恢复为默认4字节对齐

这时候sizeof(struct test)的值为8。

同理:使用__attribute__((packed))

#define PACKED __attribute__((packed))
struct PACKED test
{
char x1;
short x2;
float x3;
char x4;
}test;

这时候sizeof( test)的值仍为8。

总结

其实,内存对齐就是定制了一套规则,以合理的利用内存空间并提高内存访问效率。
编译器通过适当增加padding,使每个成员的访问都在一个指令里完成,而不需要多次访问再拼接。
是一个以空间换时间的过程。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,376评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,126评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,966评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,432评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,519评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,792评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,933评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,701评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,143评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,488评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,626评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,292评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,896评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,742评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,977评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,324评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,494评论 2 348