为什么要内存对齐 Data alignment: Straighten up and fly right

    为了速度和正确性,请对齐你的数据.

    概述:对于所有直接操作内存的程序员来说,数据对齐都是很重要的问题.数据对齐对你的程序的表现甚至能否正常运行都会产生影响.就像本文章阐述的一样,理解了对齐的本质还能够解释一些处理器的"奇怪的"行为.

 

内存存取粒度

   程序员通常倾向于认为内存就像一个字节数组.C及其衍生语言中,char * 用来指代"一块内存",甚至在JAVA中也有byte[]类型来指代物理内存.

 


Figure 1. 程序员是如何看内存的

 

   然而,你的处理器并不是按字节块来存取内存的.它一般会以双字节,四字节,8字节,16字节甚至32字节为单位来存取内存.我们将上述这些存取单位称为内存存取粒度.

 


Figure 2. 处理器是如何看内存的

 

   高层(语言)程序员认为的内存形态和处理器对内存的实际处理方式之间的差异产生了许多有趣的问题,本文旨在阐述这些问题.

   如果你不理解内存对齐,你编写的程序将有可能产生下面的问题,按严重程度递增:

  • 程序运行速度变慢
  • 应用程序产生死锁
  • 操作系统崩溃
  • 你的程序会毫无征兆的出错,产生错误的结果(silently fail如何翻译?)

 

内存对齐基础

   为了说明内存对齐背后的原理,我们考察一个任务,并观察内存存取粒度是如何对该任务产生影响的.这个任务很简单:先从地址0读取4个字节到寄存器,然后从地址1读取4个字节到寄存器.

   首先考察内存存取粒度为1byte的情况:

 


Figure 3. 单字节存取

 

   这迎合了那些天真的程序员的观点:从地址0和地址1读取4字节数据都需要相同的4次操作.现在再看看存取粒度为双字节的处理器(像最初的68000处理器)的情况:

 


Figure 4. 双字节存取

 

   从地址0读取数据,双字节存取粒度的处理器读内存的次数是单字节存取粒度处理器的一半.因为每次内存存取都会产生一个固定的开销,最小化内存存取次数将提升程序的性能.

   但从地址1读取数据时由于地址1没有和处理器的内存存取边界对齐,处理器就会做一些额外的工作.地址1这样的地址被称作非对齐地址.由于地址1是非对齐的,双字节存取粒度的处理器必须再读一次内存才能获取想要的4个字节,这减缓了操作的速度.

   最后我们再看一下存取粒度为4字节的处理器(68030,PowerPC® 601)的情况:


Figure 5. 四字节存取

 

   在对齐的内存地址上,四字节存取粒度处理器可以一次性的将4个字节全部读出;而在非对齐的内存地址上,读取次数将加倍.

   既然你理解了内存对齐背后的原理,那么你就可以探索该领域相关的一些问题了.

懒惰的处理器

   处理器对非对齐内存的存取有一些技巧.考虑上面的四字节存取粒度处理器从地址1读取4字节的情况,你肯定想到了下面的解决方法:


Figure 6. 处理器如何处理非对齐内存地址

 

   处理器先从非对齐地址读取第一个4字节块,剔除不想要的字节,然后读取下一个4字节块,同样剔除不要的数据,最后留下的两块数据合并放入寄存器.这需要做很多工作.

   有些处理器并不情愿为你做这些工作.

   最初的68000处理器的存取粒度是双字节,没有应对非对齐内存地址的电路系统.当遇到非对齐内存地址的存取时,它将抛出一个异常.最初的Mac OS并没有妥善处理这个异常,它会直接要求用户重启机器.悲剧.

   随后的680x0系列,68020,放宽了这个的限制,支持了非对齐内存地址存取的相关操作.这解释了为什么一些在68020上正常运行的旧软件会在68000上崩溃.这也解释了为什么当时一些老Mac编程人员会将指针初始化成奇数地址.在最初的Mac机器上如果指针在使用前没有被重新赋值成有效地址,Mac会立即跳到调试器.通常他们通过检查调用堆栈会找到问题所在.

   所有的处理器都使用有限的晶体管来完成工作.支持非对齐内存地址的存取操作会消减"晶体管预算",这些晶体管原本可以用来提升其他模块的速度或者增加新的功能.

   以速度的名义牺牲非对齐内存存取功能的一个例子就是MIPS.为了提升速度,MIPS几乎废除了所有的琐碎功能.

    PowerPC各取所长.目前所有的PowPC都硬件支持非对齐的32位整型的存取.虽然牺牲掉了一部分性能,但这些损失在逐渐减少.

   另一方面,现今的PowPC处理器缺少对非对齐的64-bit浮点型数据的存取的硬件支持.当被要求从非对齐内存读取浮点数时,PowerPC会抛出异常并让操作系统来处理内存对齐这样的杂事.软件解决内存对齐要比硬件慢得多.

速度

   下面编写一些测试来说明非对齐内存对性能造成的损失.过程很简单:从一个10MB的缓冲区中读取,取反,并写回数据.这些测试有两个变量:

  1. 处理缓冲区的处理粒度,单位bytes. 一开始每次处理1个字节,然后2个字节,4个字节和8个字节.
  1. 缓冲区的对准. 用每次增加缓冲区的指针来交错调整内存地址,然后重新做每个测试.

   这些测试运行在800MHzPowerBook G4.为了最小化中断引起的波动,这里取十次结果的平均值.第一个是处理粒度为单字节的情况:

 

Listing 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个字节,这将使存取次数减半:

 

Listing 2.每次处理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微秒--Munge838%.如果缓冲区不是对齐的,处理时间会增加到66385微秒--比对齐情况下慢了27%.下图展示了对齐内存和非对齐内存之间的性能对比.

 


Figure7. 单字节存取 vs.双字节存取

 

   第一个让人注意到的现象是单字节存取结果很均匀,且都很慢.第二个是双字节存取时,每当地址是单数时,变慢的27%就会出现.

   下面加大赌注,每次处理4个字节:

 

Listing 3. 每次处理4个字节

 

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微秒.因此,在所测试的机器上,非对齐地址的四字节存取速度比对齐地址的双字节存取速度要慢.

 


Figure8. 单字节vs.双字节vs.四字节存取

 

现在来最恐怖的:每次处理8个字节:

 

Listing 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微秒--大约比对齐的Munge3210%.但是,在非对齐缓冲区上的处理时间是让人惊讶的1841155微秒--比对齐的慢了两个数量级,慢了足足4610%.

   怎么回事?因为我们现今所使用的PowerPC缺少对存取非对齐内存的浮点数的硬件支持.对每次非对齐内存的存取,处理器都抛出一个异常.操作系统获取该异常并软件实现内存对齐.下图显示了非对齐内存存取带来的不利后果.

 


Figure 9. 多字节存取对比

 

   单字节,双字节和四字节的细节都被掩盖了.或许去除顶部以后的图形,如下图,更清晰:

 


Figure 10. 多字节存取对比 #2

 

   在这些数据背后还隐藏着一个微妙的现象.比较8字节粒度时边界是4的倍数的内存的存取速度:

 


Figure10. 多字节存取对比 #3

 

   你会发现8字节粒度时边界为412字节的内存存取速度要比相同情况下的42字节粒度的慢.即使PowerPC硬件支持4字节对齐的8字节双浮点型数据的存取,你还是要承担额外的开销造成的损失.诚然,这种损失绝不会像4610%那么大,但还是不能忽略的.这个实验告诉我们:存取非对齐内存时,大粒度的存取可能会比小粒度存取还要慢.


The End.


关于文章:文章并没有翻译完,还有一点关于原子性和Altivec的内容,以我现在的水平还研究不了那些东西.有兴趣自己看.

原文地址:点击打开链接

关于翻译:最大的难点在于penalty该如何翻译,文中我都绕开了,希望指教.

关于内容:如果想了解实际的结构体的内存对齐,请参考我的博文:点击打开链接

龙西村原创,严禁用于商业用途,转载请注明出处。

展开阅读全文

Alignment

09-18

DescriptionnnIn the army, a platoon is composed by n soldiers. During the morning inspection, the soldiers are aligned in a straight line in front of the captain. The captain is not satisfied with the way his soldiers are aligned; it is true that the soldiers are aligned in order by their code number: 1 , 2 , 3 , . . . , n , but they are not aligned by their height. The captain asks some soldiers to get out of the line, as the soldiers that remain in the line, without changing their places, but getting closer, to form a new line, where each soldier can see by looking lengthwise the line at least one of the line's extremity (left or right). A soldier see an extremity if there isn't any soldiers with a higher or equal height than his height between him and that extremity. nnWrite a program that, knowing the height of each soldier, determines the minimum number of soldiers which have to get out of line. nInputnnOn the first line of the input is written the number of the soldiers n. On the second line is written a series of n floating numbers with at most 5 digits precision and separated by a space character. The k-th number from this line represents the height of the soldier who has the code k (1 <= k <= n). nnThere are some restrictions: n• 2 <= n <= 1000 n• the height are floating numbers from the interval [0.5, 2.5] nOutputnnThe only line of output will contain the number of the soldiers who have to get out of the line.nSample Inputnn8n1.86 1.86 1.30621 2 1.4 1 1.97 2.2nSample Outputnn4 问答

都是对齐(Data Alignment)惹的祸

12-22

都是对齐(Data Alignment)惹的祸rnrn--Lewolfrnrn在CSDN上曾经两次碰到网友说机器编译的代码给bool或者AnsiString赋值不成功,我也曾经遇到类似的问题,但是没有仔细去研究,身边很多人都遇到类似的问题,常抱怨“BCB太不稳定”,“太差劲了”,但我想,C++毕竟是C++,不是一些弱智语言像VB之类的,连类型转换都不要。C++的一些特点也许注定你想使用之,就得多花一些精力。rnrn前几天给Aweay发了一个字节对齐的Cruiser客户端测试,结果发现订票按钮始终为无效状态,当时怀疑是由于不同的对齐方式导致的,但是这个程序在我自己的机器上没有问题,后来在优化Cruiser的服务程序时,无意发现了对齐修改方式会产生恶果。rnrn看一下下面这个类rnrnclass TBugedClassrnrnpublic:rn bool Test;rn AnsiString Text;rn int __fastcall GetOffset(void);rn;rnrnint __fastcall TBugedClass::GetOffset(void)rnrn return (int)&Text - (int)this;rn rnrnGetOffset用来计算Text在类中的偏移量。rn再一个Exe中使用rnrnvoid __fastcall TForm2::Button1Click(TObject *Sender)rnrn TBugedClass Test;rn int Temp = (int)&Test.Text - (int)&Test;rn Memo1->Lines->Add(AnsiString("The Offset from Exe is ") + Temp);rn Memo1->Lines->Add(AnsiString("The Offset from Class is ") + Test.GetOffset());rnrnrn结果是两个方式获取的偏移量是相等的,如果修改exe工程的对齐方式,结果也是相等,但和未修改之前不相等。这是正确的,C++提供了选择数据对齐方式的能力,Windows中推荐使用32位对齐,有助于提高寻址速度。rnrn但现在的问题是如果这两个文件不是同时编译,在编译时选择了不相同的对齐方式,或者在工程中设置Local Option使得BugedClass和使用该类实例的代码采用了不同的对齐方式,那么你的程序肯定会出错的。例如上面的代码中,我们将BugedClass所在的单元单独编译,然后将cpp文件从工程中移去,同时加入obj文件,编译工程是没有问题的,运行也是正常的,此时修改对齐方式,重新编译,情况出现了,两次获取的Text偏移量是不相等的。rnrn我不知道这个算不算是Bug,在BCB中,大多数情况都是这样的,比如一个VCL组件,在使用的时候,你可以放心的确定程序代码和组件的代码不是同时编译的,除非你硬是将VCL的相关源码加入到工程中(但这种情况下VCL组件也不一定是现场编译的)。因此这种情况也就可能随时出现,然而事实并非如此,无论你怎么修改程序的对齐方式,其编译的结果都是正确的,只要你使用的都是BCB自己带的标准组件!!rnrn何故??????rnrn我们再来看一个Object Pascal的类:rnrnunit TestBug;rninterfacernuses Windows;rntypern TBugClassOP = classrn publicrn Test: Boolean;rn C: CHAR;rn Text :String;rn end;rnimplementationrnend.rnrn这是一个最简单的(VCL风格,Object Pascal中没有指定基类时,缺省的为TObject)OP类。将这个单元添加到工程中,无论如何修改对齐方式,下面的代码运算结果始终是一致的。rnrnvoid __fastcall TForm2::Button3Click(TObject *Sender)rnrn TBugClassOP * Test = new TBugClassOP;rn int Temp = (int)&Test->Text - (int)Test;rn Memo1->Lines->Add(AnsiString("The Offset Of OPClass from exe is ") + Temp);rn delete Test;rnrnrnText的偏移量始终是8,也就是说不管你工程的对齐方式如何,Object Pascal将始终以4字节对齐的,(TObject的第一个成员是自身的TClass类型信息,供RTTI使用的,而且是一个隐含的成员,由编译器自己生成,关于TObject的TClass信息参考ChinaBCB上的一篇关于BCB中RTTI的帖子)这也就是使用标准BCB的组件不会产生有歧义的代码和运行错误。rnrn但是我想,任何一个BCB Fans不会局限在使用标准BCB组件和一大堆的OP第三方组件中的,很多时候都需要使用自己设计的纯C++编写的TObject派生类。或者有很多自己定义的数据类型,只要有这样的使用,就可能有这样的问题存在。rnrn此时我感到有些伤感了,BCB果然是二奶的孩子,Borland使用VCL时显然是考虑了对齐方式不一致的情况,因此,在BCB中使用Object Pascal的代码就不会出现问题,也因此上VCL和其使用代码采用了紧密偶合(必须有hpp、h、Lib、BPI等文件)的方式,而且没有出现任何问题,但Borland却没有考虑BCB的使用者会使用BCB作为组件的开发工具,而且Borland还学人家在编译器中加入对齐方式的选项,却不对VCL类进行对齐方式的控制,VCL又不像ActiveX使用弱偶合关系。。。。rnrn问题是存在的,但总归还是需要解决的,如果是使用BCB作为开发工具的话,建议:rn1.不要轻易对数据的对齐方式作优化。rn2.尽量不要在BCB写的组件中使用public的直接成员访问,取而代之的是使用属性和其Get方法,Get方法会在编译组件的时候按照设置对齐编译,程序中使用这样的属性,不会产生歧义的代码,Set方法和Get方法是对应的,可以保证数据是安全的。rn3.如果作通讯程序,尽量不要投省事,使用class,或者Struct整体拷贝,整体拷贝会将数据对齐后的空位也拷贝到待发数据中,实际上造成通讯数据的格式和编译选项相关(此乃程序大忌)。 论坛

Fly Monkey

04-21

Fly Monkey is a well-known program of the circus, which is performed by the beautiful and lovely monkey Pipi. In the program of Fly Monkey, there are two long steel wires in air. Pipi is initially located on one of the wires, and her objective is reaching another wire. Pipi must first crawl over the wire from her initial position by some distance, and then jump to some position of another wire. Since Pipi moves quite fast, the trace of her jumping can be considered as a stright line. To prevent from dangers, Pipi tends to shorten her jumping distance, while cannot crawl by more than distance d in advance to save the time. In this conditions, how long Pipi must jump at least?nnInputnnInput contains multiple test cases. Each test case contains 16 real numbers in one line, which are x1, y1, z1, x2, y2, z2, x3, y3, z3, x4, y4, z4, xp, yp, zp, d. (x1,y1,z1)-(x2,y2,z2) are the coordinates of the two ends of the first wire, (x3,y3,z3)-(x4,y4,z4) are the coordinates of the two ends of the second wire, (xp,yp,zp) is the coordinate of the initial position of PiPi, d is the maximum distance Pipi can crawl. It is guaranteed that Pipi must locate on the first wire, and the lengthes of the two wires are positive. But wires may intersect or even overlap.nnOutputnnThere is only one line for each test case, which contains a real number. Three digits after decimal point are preserved by rounding.nnSample Inputnn0.0 0.0 0.0 4.0 4.0 0.0 4.0 0.0 1.0 0.0 4.0 1.0 2.0 2.0 0.0 10.0nSample Outputnn1.000 问答

没有更多推荐了,返回首页