内存对齐的规则以及作用(转)

内存对齐的规则以及作用

Posted on 2009-03-16 09:36 蜗牛先生 阅读(23409) 评论(19) 编辑 收藏 wps3BC9.tmp

首先由一个程序引入话题:

1 //环境:vc6 + windows sp2
2 //程序1
3 #include <iostream>
4
5 using namespace std;
6
7 struct st1 
8 {
9 char a ;
10 int  b ;
11 short c ;
12 };
13
14 struct st2
15 {
16 short c ;
17 char  a ;
18 int   b ;
19 };
20
21 int main()
22 {
23     cout<<"sizeof(st1) is "<<sizeof(st1)<<endl;
24     cout<<"sizeof(st2) is "<<sizeof(st2)<<endl;
25 return 0 ;
26 }
27

程序的输出结果为:

sizeof(st1) is 12

  sizeof(st2) is 8

问题出来了,这两个一样的结构体,为什么sizeof的时候大小不一样呢?

本文的主要目的就是解释明白这一问题。

内存对齐,正是因为内存对齐的影响,导致结果不同。

对于大多数的程序员来说,内存对齐基本上是透明的,这是编译器该干的活,编译器为程序中的每个数据单元安排在合适的位置上,从而导致了相同的变量,不同声明顺序的结构体大小的不同。

那么编译器为什么要进行内存对齐呢?程序1中结构体按常理来理解sizeof(st1)和sizeof(st2)结果都应该是7,4(int) + 2(short) + 1(char) = 7 。经过内存对齐后,结构体的空间反而增大了。

在解释内存对齐的作用前,先来看下内存对齐的规则:

1、 对于结构的各个成员,第一个成员位于偏移为0的位置,以后每个数据成员的偏移量必须是min(#pragma pack()指定的数,这个数据成员的自身长度) 的倍数。

2、 在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行。

#pragma pack(n) 表示设置为n字节对齐。 VC6默认8字节对齐

以程序1为例解释对齐的规则

St1 :char占一个字节,起始偏移为0 ,int 占4个字节,min(#pragma pack()指定的数,这个数据成员的自身长度) = 4(VC6默认8字节对齐),所以int按4字节对齐,起始偏移必须为4的倍数,所以起始偏移为4,在char后编译器会添加3个字节的额外字节,不存放任意数据。short占2个字节,按2字节对齐,起始偏移为8,正好是2的倍数,无须添加额外字节。到此规则1的数据成员对齐结束,此时的内存状态为:

oxxx|oooo|oo

0123 4567 89 (地址)

x表示额外添加的字节)

共占10个字节。还要继续进行结构本身的对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行,st1结构中最大数据成员长度为int,占4字节,而默认的#pragma pack 指定的值为8,所以结果本身按照4字节对齐,结构总大小必须为4的倍数,需添加2个额外字节使结构的总大小为12 。此时的内存状态为:

oxxx|oooo|ooxx

0123 4567 89ab  (地址)

到此内存对齐结束。St1占用了12个字节而非7个字节。

St2 的对齐方法和st1相同,读者可自己完成。

内存对齐的主要作用是:

1、 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

2、 性能原因:经过内存对齐后,CPU的内存访问速度大大提升。具体原因稍后解释。

图一:
wps3BCA.tmp

这是普通程序员心目中的内存印象,由一个个的字节组成,而CPU并不是这么看待的。

图二:

wps3BCB.tmp

CPU把内存当成是一块一块的,块的大小可以是2,4,8,16字节大小,因此CPU在读取内存时是一块一块进行读取的。块大小成为memory access granularity(粒度) 本人把它翻译为“内存读取粒度” 。

假设CPU要读取一个int型4字节大小的数据到寄存器中,分两种情况讨论:

1、数据从0字节开始

2、数据从1字节开始

再次假设内存读取粒度为4。

图三:

wps3BCC.tmp

当该数据是从0字节开始时,很CPU只需读取内存一次即可把这4字节的数据完全读取到寄存器中。

    当该数据是从1字节开始时,问题变的有些复杂,此时该int型数据不是位于内存读取边界上,这就是一类内存未对齐的数据。

图四:

wps3BCD.tmp

此时CPU先访问一次内存,读取0—3字节的数据进寄存器,并再次读取4—5字节的数据进寄存器,接着把0字节和6,7,8字节的数据剔除,最后合并1,2,3,4字节的数据进寄存器。对一个内存未对齐的数据进行了这么多额外的操作,大大降低了CPU性能。

    这还属于乐观情况了,上文提到内存对齐的作用之一为平台的移植原因,因为以上操作只有有部分CPU肯干,其他一部分CPU遇到未对齐边界就直接罢工了。

图片来自:Data alignment: Straighten up and fly right

如大家对内存对齐对性能的具体影响情况,可以参考上文。


下面是翻译IBM上的一篇文章


数据对齐:展翅高飞

使您的数据在速度和正确性

数据对齐是一个重要的问题对于所有程序员直接使用内存。数据对齐影响您的软件如何执行,即使你的软件运行。正如本文所演示的那样,理解的本质对齐也可以解释的一些“奇怪”的行为有些处理器。

PDF(434 KB) |

分享:

乔纳森Rentzsch (jon.dw@redshed.net),总统,红色的软件

2005年2月08

 

内存访问粒度

程序员习惯于认为记忆是一个简单的字节数组。在C和它的后代, char*无处不在的意思“一块记忆”,甚至是Java™有它吗 byte[]代表原始内存类型。

图1所示。程序员如何看待记忆

wps3BDF.tmp

然而,你的计算机的处理器并不在byte-sized块读取和写入内存。相反,它访问内存中2,4,8 - 16,甚至32字节的块。我们叫一个处理器访问内存大小的内存访问粒度。

图2。处理器内存怎么看

wps3BE0.tmp

高级程序员的思维方式之间的差异的内存和现代处理器如何处理提出了有趣的问题,本文探讨了记忆。

如果你不理解和地址对齐问题在您的软件,下面的场景,严重程度递增的顺序,都是可能的:

· 你的软件将运行慢。

· 您的应用程序将锁定。

· 您的操作系统会崩溃。

· 你的软件会默默地失败,产生不正确的结果。

回到顶部

对齐的基本面

为了说明对齐背后的原则,检查一个常数的任务,它是如何影响处理器的内存访问粒度。这个任务很简单:第一次读四个字节从地址0到处理器的寄存器。然后读四个字节地址1到相同的寄存器。

首先检查会发生什么在一个处理器1字节内存访问粒度:

图3。单字节内存访问粒度

wps3BE1.tmp

这与天真的程序员的模型相对应的记忆是如何工作的:需要相同的四个内存访问读取地址0,从地址1。现在看看会发生什么在一个处理器上两字节的粒度,像最初的68000:

图4。双字节内存访问粒度

wps3BE2.tmp

阅读从地址0时,处理器两字节粒度需要一半数量的内存访问作为一个处理器1字节的粒度。因为每个内存访问需要一个固定的开销,减少访问的数量真的可以帮助提高性能。

然而,注意当读取地址1。因为地址不均匀落在处理器的内存访问边界,处理器有额外的工作要做。这样一个地址被称为一个对齐的地址。因为地址1未对齐,处理器与两字节粒度必须执行一个额外的内存访问,减缓操作。

最后,检查会发生什么在一个处理器上四字节内存访问粒度,如68030或PowerPC®601:

图5。Quad-byte内存访问粒度

wps3BE3.tmp

处理器的四字节的粒度可以吸收四字节对齐的地址与一个阅读。还要注意,阅读从一个对齐的地址双打访问数。

现在你理解背后的基本原理一致的数据访问,您可以探索调整相关的一些问题。

回到顶部

懒惰的处理器

处理器必须执行一些技巧当要求访问一个对齐的地址。回到阅读四个字节地址1的例子在处理器四字节的粒度,您可以准确地计算出需要做什么:

图6。处理器如何处理对齐内存访问吗

wps3BE4.tmp

处理器需要阅读的第一块对齐的地址和移出“意外”字节从第一块。那么它需要读的第二块对齐的地址和移出它的一些信息。最后,两个是合并在一起的位置注册。这是一个大量的工作。

一些处理器只是不愿意为你做所有的工作。

最初的68000年是一个处理器两字节的粒度和缺乏电路应对对齐的地址。当面对这样的一个地址,处理器将抛出异常。最初的Mac OS没有把这看做是善意的例外,并通常会要求用户重新启动机器。哎哟。

系列处理器在680年晚些时候x0,如68020年,取消了这一限制,执行必要的为你工作。这就解释了为什么一些旧的软件,68020年68020起碰撞事故工作。方式的时候,它也解释了为什么一些旧Mac程序员与奇怪的地址指针初始化。原始Mac,如果指针访问不被重新分配到一个有效的地址,Mac会立即进入调试器。他们可以检查调用堆栈链并找出错误在哪里。

所有的处理器有一个有限的晶体管数量来完成工作。添加对齐的地址访问支持削减到这个“晶体管预算。“这些晶体管否则可以用来制造其他部分的处理器工作更快,或添加新功能。

处理器的一个例子,牺牲对齐的地址访问速度是MIPS的名义支持。MIPS处理器是一个很好的例子,做了几乎所有的轻浮的名义完成实际工作得更快。

PowerPC混合方法。迄今为止的所有PowerPC处理器硬件支持不结盟的32位整数的访问。而你仍然支付对齐访问的性能损失,它往往是小。

另一方面,现代PowerPC处理器缺乏硬件支持64位浮点对齐访问。当被要求从内存加载未对齐浮点数,现代PowerPC处理器将抛出一个异常,软件的操作系统执行对齐的家务。执行校准软件比硬件执行慢得多。

回到顶部

速度

编写一些测试说明了对齐内存访问的性能损失。测试很简单:你看,否定,回信ten-megabyte缓冲区的数据。这些测试有两个变量:

1. 大小,以字节为单位,你处理缓冲区。首先你要处理一次一个字节缓冲区。然后你将进入两个、四个,8个字节。

2. 缓冲区的对齐。你会交错排列的缓冲递增指向缓冲区的指针并再次运行每一个测试。

这些测试在800 MHz G4强力笔记本电脑进行。从中断处理帮助正常化性能波动,每个测试运行十倍,保持运行的平均值。首先是作用于单个字节的测试时间:

清单1。绿豆一次一个字节的数据

void Munge8( void *data, uint32_t size ) {

    uint8_t *data8 = (uint8_t*) data;

    uint8_t *data8End = data8 + size;

    while( data8 != data8End ) {

        *data8++ = -*data8;

    }

}

平均67364微秒才执行这个函数。现在修改工作两个字节一次而不是一次一个字节,这将减少一半内存访问的数量:

清单2。绿豆两个字节的数据

void Munge16( void *data, uint32_t size ) {

    uint16_t *data16 = (uint16_t*) data;

    uint16_t *data16End = data16 + (size >> 1); /* Divide size by 2. */

    uint8_t *data8 = (uint8_t*) data16End;

    uint8_t *data8End = data8 + (size & 0x00000001); /* Strip upper 31 bits. */

    while( data16 != data16End ) {

        *data16++ = -*data16;

    }

    while( data8 != data8End ) {

        *data8++ = -*data8;

    }

}

这个函数把48765微秒过程同样ten-megabyte缓冲——比Munge8快38%。然而,缓冲是一致的。如果缓冲是对齐的,所需的时间增加到66385微秒,约27%的速度惩罚。下面的图表说明了性能的对齐的内存访问模式和不结盟的访问:

图7。单字节和双字节访问访问

wps3BF5.tmp

你注意到的第一件事就是访问一次一个字节的内存是均匀缓慢。第二项感兴趣的是,当访问两个字节的内存,只要地址能被2整除,并不均匀,27%的攻击速度惩罚你的脑袋。

现在提高赌注,过程一次缓冲四个字节:

清单3。绿豆数据的四个字节

void Munge32( void *data, uint32_t size ) {

    uint32_t *data32 = (uint32_t*) data;

    uint32_t *data32End = data32 + (size >> 2); /* Divide size by 4. */

    uint8_t *data8 = (uint8_t*) data32End;

    uint8_t *data8End = data8 + (size & 0x00000003); /* Strip upper 30 bits. */

    while( data32 != data32End ) {

        *data32++ = -*data32;

    }

    while( data8 != data8End ) {

        *data8++ = -*data8;

    }

}

这个函数过程一个对齐43043微秒的缓冲和不结盟的缓冲区在55775微秒,分别。因此,在这个测试机器,一次访问内存对齐四个字节是低于一次访问内存对齐两个字节:

图8。单-和双-与quad-byte访问

wps3BF6.tmp

现在的恐怖故事:一次处理缓冲八个字节。

清单4。绿豆一次8个字节的数据

void Munge64( void *data, uint32_t size ) {

    double *data64 = (double*) data;

    double *data64End = data64 + (size >> 3); /* Divide size by 8. */

    uint8_t *data8 = (uint8_t*) data64End;

    uint8_t *data8End = data8 + (size & 0x00000007); /* Strip upper 29 bits. */

    while( data64 != data64End ) {

        *data64++ = -*data64;

    }

    while( data8 != data8End ) {

        *data8++ = -*data8;

    }

}

Munge64流程39085微秒,大约10%的对齐的缓冲速度比处理缓冲区四个字节。然而,处理未对齐缓冲需要惊人的1841155微秒,两个数量级低于对齐访问,一个杰出的性能损失4610% !

发生了什么事?因为现代PowerPC处理器缺乏硬件支持不结盟的浮点访问,为每个对齐访问处理器将抛出一个异常。操作系统捕获这个异常并在软件执行的一致性。这里有一���图表说明了点球,当它发生时:

图9。多字节访问比较

wps3BF7.tmp

惩罚一个、两个、四字节对齐访问相比就是小巫见大巫了可怕的对齐eight-byte点球。也许这个图表,删除(因此这两个数字之间的巨大鸿沟),将更为清晰:

图10。多字节访问比较# 2

wps3BF8.tmp

还有一个微妙的洞察隐藏在这些数据。比较eight-byte访问速度四字节边界:

图11。多字节访问比较# 3

wps3BF9.tmp

注意访问内存一次八个字节4——12字节的边界是低于阅读相同的内存四甚至两个字节。虽然powerpc硬件支持四字节对齐eight-byte双打,你仍然支付如果你使用,支持性能损失。当然,这附近没有地方4610%的点球,但又确实很明显。这个故事的寓意是:访问内存中大块小块中可以比访问内存慢,如果访问不是一致的。

回到顶部

原子性

所有现代处理器提供原子指令。这些特殊的指令同步两个或多个并发任务的关键。顾名思义,原子指令必须是不可分割的,这就是为什么他们是如此方便的同步:他们不能被抢占。

事实证明,为了让原子指令执行正确,地址你通过他们必须至少四字节对齐。这是因为一个微妙的原子之间的相互作用和虚拟内存指令。

如果一个地址对齐,它需要至少两个内存访问。但是如果所需的数据跨越两个页面的虚拟内存?这可能导致一种情况第一页是居民,而不是最后一页。访问后,中间的指令,将生成的页面错误,执行虚拟内存管理换入代码,破坏指令的原子性。让事情简单而正确的,68 k和PowerPC要求自动操纵地址总是至少四字节对齐。

不幸的是,PowerPC不抛出异常时自动存储到一个对齐的地址。相反,这家店只是总是失败。这是不好的,因为大多数原子函数写入重试失败的商店,在假设他们被抢占。这两种情况结合到您的程序将进入一个无限循环,如果你试图自动存储到一个对齐的地址。哦。

回到顶部

Altivec

Altivec就是速度。对齐内存访问减缓了宝贵的晶体管处理器和成本。因此,Altivec工程师MIPS的营销手法,只是不支持对齐内存访问。因为Altivec sixteen-byte块一次,所有的地址传递给Altivec必须sixteen-byte对齐。可怕的是如果你的地址是不一致的。

Altivec不会抛出异常,警告你对齐的地址。相反,Altivec只是忽略低四位的地址和费用,操作错误的地址。这意味着您的程序可能会默默地腐败内存或显式地返回不正确的结果,如果你不确定你所有的数据是一致的。

有一个优势Altivec bit-stripping方式。因为你不需要显式地截断(align-down)一个地址,这种行为可以节省你一两个指令时将地址给处理器。

这并不是说Altivec不能进程对齐内存。你可以找到详细说明如何Altivec编程环境手册(见资源)。它需要更多的工作,但是因为记忆是如此缓慢和处理器相比,这种恶作剧的开销很低。

回到顶部

结构调整

检查以下结构:

清单5。一个无辜的结构

void Munge64( void *data, uint32_t size ) {

typedef struct {

    char    a;

    long    b;

    char    c;

}   Struct;

这个结构在字节的大小是什么?许多程序员会回答“6字节。“这是有道理的:一个字节 a,四个字节 b和另一个字节 c。1 + 4 + 1 = 6。在内存中如何布置:

表1。结构大小的字节

字段类型

字段名

磁场抵消

字段长度

场结束

char

a

0

1

1

long

b

1

4

5

char

c

5

1

6

总字节数:

6

然而,如果你问你的编译器 sizeof( Struct )答案,很有可能你会回来会大于6,也许八甚至24。有两个原因:向后兼容性和效率。

首先,向后兼容性。还记得68000年的处理器有两字节内存访问粒度,并将抛出一个异常时遇到一个奇怪的地址。如果你读或写 b,你会试图访问一个奇怪的地址。如果调试器没有安装,旧的Mac OS将抛出一个系统错误对话框和一个按钮:重新启动。呵!

因此,制定你的领域而不是你写的方式,这样编译器的结构 b和 c甚至会驻留在地址:

表2。结构与编译器填充

字段类型

字段名

磁场抵消

字段长度

场结束

char

a

0

1

1

填充

1

1

2

long

b

2

4

6

char

c

6

1

7

填充

7

1

8

总字节数:

8

填充物的添加,否则未使用的空间结构领域所需的方式排列。现在,当在68020年推出了内置的硬件支持对齐内存访问,这填充是不必要的。然而,它没有伤害,甚至帮助性能。

第二个原因是效率。如今,在PowerPC机器上,两字节对齐是不错,但四字节或eight-byte更好。你可能不在乎了,原来68000窒息对齐结构,但是你可能关心的潜在性能损失4610%,如果一个 double字段不一致坐在你的设计结构。

回到顶部

结论

如果你不理解和明确的代码数据对齐:

· 您的软件可能会影响performance-killing对齐内存访问异常,调用非常昂贵的定位异常处理程序。

· 您的应用程序可能试图自动存储到一个对齐的地址,导致应用程序锁定。

· 您的应用程序可能试图通过Altivec对齐的地址,导致Altivec读取和/或写入了错误记忆的一部分,默默地腐蚀数据或产生不正确的结果。

回到顶部

学分

为反馈,感谢亚历克斯·罗森博格和伊恩•马特槽FastTimes时机库,和杜安海耶斯提供一群测试机器。

资源

· 如果你有兴趣学习更多关于原子性,一定要检查使用PowerPC原子指令保存您的代码崩溃(developerWorks,2004年11月)。

· 丹·伯恩斯坦评论PPC对齐和技巧让GCC做正确的事。

· 对齐是很多开发人员可能想知道Power体系结构;developerWorks跑一个开发人员指南的权力架构,这就使指针指向其他相关问题(developerWorks,2004年3月)。

· 对齐是一个多线程代码中可能面临的许���问题,在电力系统或设备驱动程序,使用共享存储。了解更多的PowerPC存储模型(developerWorks,2002年8月)。

· 更多地了解GNU GCC在各种不同的平台上,看到的链接和选择阅读.

· PPC编程,明白了PowerPC编译器作者的指南.

· 更多细节可以找到64位PowerPC64位PowerPC精灵应用程序二进制接口补充.

· 更在这两个极端的中间,看到的了解64位PowerPC架构(developerWorks,2004年10月)。

· C程序员,是吗?你可能也会感兴趣可重入函数用于安全的信号处理(developerWorks,2005年1月)介绍了如何以及何时使用可重入性,和大卫·惠勒的在developerWorks Linux专区获得程序员列.

· 在IBM PowerPC 970第一部分:设计理念和前端是非常有用的,如果你想了解更多关于处理器本身;第二部分:执行核心进入Altivec在一些细节。SIMD架构讨论了3 1/2 SIMD架构在和奔腾4和G4e:一个架构的比较(所有Ars Technica)。

· 下载一个IBM PowerPC 405评估工具演示一个SoC在模拟环境中,或者只是探索的完全授权版本Power体系结构技术。

评论

 

posted @ 2016-09-29 09:58  张飞online  阅读(729)  评论(0编辑  收藏  举报