接触内存对齐这个概念,也有三四年了。不过由于我工作后一直做游戏服务器,都是在x86架构的机子上写代码,也没怎么注意内存对齐。使用最多的估计也就是面试时经常问结构体大小。最近在写自己服务器框架的二进流读写模块时,整理了下这方面的内容。本方不会涉及基本概念。
内存对齐只是指数据存储在内存时的起始地址是否是某个值的整数倍。如果只是放在内存中,是否对齐本身并没有什么问题。问题是读取、写入的时候。访问一个不对齐的数据(unaligned memory access)可能会导致程序运行效率慢,结果出错,甚至是程序当掉。那这些情况是怎么出现的呢?
我们都知道,程序最终都是以CPU指令来运行的。参考:http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.faqs/ka15414.html,我们知道ARM CPU有下面几条指令:
LDRB/STRB - address must be byte aligned
LDRH/STRH - address must be 2-byte aligned
LDR/STR - address must be 4-byte aligned
LDRB/STRB字节加载、存储指令
LDRH/STRH半字(即2byte,不是半字节)加载、存储指令
LDR/STR 字加载、存储指令
也就是说,当我们从内存中存取数据时,要调用上面的指令。而这些指令在设计时,较老的CPU由于考虑了硬件、效率等等问题,要求访问的内存必须是对齐的。现在假如我声明了一个内存缓冲区char *buffer[1024],系统给它分配的地址是0x00001000,可以看到,这个地址都是符合1、2、4字节对齐的。接着我从网络接收了一段数据,放到这个缓冲区里。现在要从缓冲区里依次取出char、int两个类型的数据:
char ch = *buffer;
int i = *reinterpret_cast<int *>(buffer+1);
运行ch = *buffer时,由于char类型的大小是1字节,CPU将调用LDRB指令,这时将检测buffer是否按1byte对齐。这里当然是对齐的,所以指令运行正常。
运行i = *reinterpret_cast<int *>(buffer+1)时,由于int类型大小是4字节,CPU将调用LDR指令,这时检测buffer+1(0x00001001)是否按4byte对齐,结果发现不对齐,CPU将报错,程序中止。
而安全的做法是这样的:
memcpy( &ch,buffer,1 );
memcpy( &i,buffer+1,4 );
你可能会问,使用memcpy,buffer+1的地址也是不对齐的,为什么就安全了呢?就像我上面所说的,数据在内存中存放时,是否对齐并不重要,重要的是你怎样去访问它。memcpy的实现本身并不简单(你在源码里看到的通过while每次拷贝一个char的只是一个例子,并不是真实的memcpy),它考虑了是否对齐。当检测到内存是对齐时,memcpy调用合适的指令(比较这里拷贝一个int,就调用LDR),一次拷贝多个字节,以提高效率。当检测到不对齐时,先调用LDRB遂个字节拷贝,直到对齐部分后再调用合适的指令拷贝。因此,在上面的例子中,它是先调用LDRB的,因为LDRB是按1byte对齐(所有的内存都按这个对齐),所以不会触发报错。但效率就要慢一点了,毕竟要拷贝几次。
内存对齐本身对程序员来说是透明的,即程序员该取变量就取变量,该存就存,编译程序时编译器会把变量按本身的平台进行对齐。况且现在的CPU都很高级,别说服务器,台式机的CPU,ARM 7以上应该也支持内存不对齐访问了。但如果你要写一个内存池(boost的ordered_pool有对齐的例子),或者使用了reinterpret_cast这种对内存直接进行操作的函数,这方面还是要注意一下,即使CPU支持,效率也会受到影响。
我在很多项目中,发现这样的写法:
#pragma pack(push,1)
struct NetPack
{
//...
};
#pragma pack(pop)
这是强制把这个结构体按1byte对齐,当有网络数据过来,直接memcpy整个结构体就可以。有趣的时,我在内核文档里发现这么一段话:https://www.kernel.org/doc/Documentation/unaligned-memory-access.txt
当我们把变量强制按1byte对齐时,编译器不会在结构体中加入任何内容来使得这个结构体符合内存对齐,而是产生一些额外的指令来让他满足当前平台的内存对齐,当然,效率还是受影响的。
本文转自xsster51CTO博客,原文链接:http://blog.51cto.com/12945177/1951667 ,如需转载请自行联系原作者