我最近考虑过对齐......这是我们通常不必考虑的事情,但我已经意识到某些处理器要求对象沿着4字节边界对齐。这究竟是什么意思,以及哪些特定系统具有对齐要求?
假设我有一个任意指针:
unsigned char* ptr
现在,我正在尝试从内存位置检索double值:
double d = **((double*)ptr);
这会导致问题吗?
答案 0 :(得分:20)
它肯定会在某些系统上造成问题。
例如,在基于ARM的系统上,您无法寻址未与4字节边界对齐的32位字。这样做会导致访问冲突异常。在x86上,您可以访问这些非对齐数据,但性能会受到一点影响,因为必须从内存中取出两个单词而不是一个单词。
答案 1 :(得分:13)
以下是Intel x86/x64 Reference Manual关于路线的说法:
4.1.1单词,双字,四字和双四字的对齐
单词,双字和四字 不需要在内存中对齐 自然界限。自然 单词边界,双字, 和四字是偶数 地址,地址可以整除 四,并均匀地解决 分别可以被8整除。 但是,要提高性能 程序,数据结构(特别是 堆栈)应该在自然对齐 边界尽可能。该 原因是处理器 需要两次内存访问才能生成 未对齐的内存访问;对齐 访问只需要一个内存 访问。单词或双字操作数 跨越4字节边界或 跨越的四字操作数 考虑8字节边界 未对齐,需要两个单独的 用于访问的存储器总线周期。
一些操作说明 双四字需要记忆 操作数要在自然界上对齐 边界。生成这些指令 一般保护例外(#GP) 如果指定了未对齐的操作数。 双重的自然边界 四字是均匀的任何地址 可被16整除。其他说明 以双四字运行 允许不对齐访问(没有 产生一般保护 例外)。但是,额外的内存 需要总线周期才能访问 来自内存的未对齐数据。
不要忘记,参考手册是负责任的开发人员和工程师的最终信息来源,因此如果您正在处理诸如Intel CPU之类的文档,请查阅参考手册中有关该问题的内容。
答案 2 :(得分:4)
对齐会影响结构的布局。考虑这个结构:
struct S {
char a;
long b;
};
在32位CPU上,此结构的布局通常为:
a _ _ _ b b b b
要求是32位值必须在32位边界上对齐。如果结构改变如下:
struct S {
char a;
short b;
long c;
};
布局将是:
a _ b b c c c c
16位值在16位边界上对齐。
有时,如果要将结构与数据格式匹配,可能需要打包结构。通过使用编译器选项或#pragma
,您可以删除多余的空格:
a b b b b
a b b c c c c
但是,访问压缩结构的未对齐成员在现代CPU上通常要慢得多,或者甚至可能导致异常。
答案 3 :(得分:4)
是的,这可能会导致许多问题。 C ++标准实际上并不能保证它能够正常工作。你不能随意在指针类型之间进行投射。
当您将char指针强制转换为双指针时,它使用reinterpret_cast
,它应用实现定义的映射。您无法保证结果指针将包含相同的位模式,或者它将指向相同的地址,或者其他任何东西。在更实际的术语中,您也无法保证您正在阅读的值正确对齐。如果数据是作为一系列字符编写的,那么它们将使用char的对齐要求。
至于什么对齐意味着,基本上只是值的起始地址应该可以被对齐大小整除。例如,地址16在1,2,4,8和16字节边界上对齐,因此在典型的CPU上,这些大小的值可以存储在那里。
地址6未在4字节边界上对齐,因此我们不应在那里存储4字节值。
值得注意的是,即使在不强制执行或需要对齐的CPU上,您通常仍会因访问未对齐的值而显着减速。
答案 4 :(得分:3)
是的,这可能会导致问题。
4对齐只是意味着指针在被视为数字地址时是4的倍数。如果指针不是所需对齐的倍数,则它是未对齐的。编译器对某些类型设置对齐限制有两个原因:
如果您遇到情况(1),并且double是4对齐的,并且您使用char *
指针尝试不是4对齐的代码,那么您很可能会获得硬件陷阱。有些硬件不会陷阱。它只是加载一个无意义的值并继续。但是,C ++标准没有定义会发生什么(未定义的行为),因此这段代码可能会使您的计算机着火。
在x86上,你永远不会遇到(1),因为标准加载指令可以处理未对齐的指针。在ARM上,没有未对齐的加载,如果你尝试加载,那么程序崩溃(如果你很幸运。有些ARM默默地失败)。
回到你的例子,问题是为什么你用一个不是4对齐的char *
来尝试这个。如果您通过double *
在那里成功写了一个双精度数,那么您将能够阅读它。因此,如果你最初有一个“正确”指针加倍,你投射到char *
并且你现在正在反击,你不必担心对齐。
但是你说任意char *
,所以我猜这不是你所拥有的。如果您从包含序列化双精度的文件中读取一大块数据,那么您必须确保满足您平台的对齐要求才能执行此操作。如果你有8个字节代表某种文件格式的double,那么你不能只是在任何偏移处将它读入char *缓冲区,然后转换为double *
。
最简单的方法是确保将文件数据读入合适的结构中。内存分配始终与任何类型的最大对齐要求保持一致,这些要求的大小足以包含这些内容。因此,如果您分配一个足够大的缓冲区来包含一个double,那么该缓冲区的开头具有double所需的任何对齐方式。那么你可以将表示double的8个字节读入缓冲区的开头,转换(或者使用union)并读取double。
或者,您可以这样做:
double readUnalignedDouble(char *un_ptr) {
double d;
// either of these
std::memcpy(&d, un_ptr, sizeof(d));
std::copy(un_ptr, un_ptr + sizeof(d), reinterpret_cast<char *>(&d));
return d;
}
这保证有效(假设un_ptr确实指向平台的有效双重表示的字节),因为double是POD,因此可以逐字节复制。如果要加载很多双打,它可能不是最快的解决方案。
如果你正在读取一个文件,那么实际上还有更多的内容,如果你担心具有非IEEE双重表示的平台,或9位字节,或其他一些不寻常的属性,可能会有存储的double表示中的非值位。但是你实际上并没有问过文件,我只是把它作为一个例子来做,而且无论如何这些平台比你要问的问题要少得多,这对于具有对齐要求的双倍来说。
最后,没有什么可以做对齐,你也有严格的别名来担心你是否通过一个与char *
不兼容的指针的转换得到double *
。但是,别名在char *
本身和其他任何内容之间有效。
答案 5 :(得分:2)
在x86上它总是会运行,当然在对齐时效率更高。
但如果你是MULTITHREADING,那么请注意读写撕裂。使用64位值,您需要一台x64机器来为线程之间提供原子读写
如果你说在另一个线程中读取值在0x00000000.FFFFFFFF和0x00000001.00000000之间递增时,那么另一个线程理论上可能读取0或1FFFFFFFF,特别是IF SAY值STRADDLED A CACHE-LINE boundary。
我推荐Duffy的“Windows上的并发编程”,因为它很好地讨论了内存模型,甚至在dot-net执行GC时也提到了多处理器上的对齐问题。你想远离安腾!
答案 6 :(得分:2)
SPARC(Solaris机器)是另一种架构(至少在过去的某些时候),如果您尝试使用未对齐的值,它将阻塞(发出SIGBUS错误)。
马丁约克的附录,malloc也与最大可能的类型对齐,即它对所有东西都是安全的,比如'new'。事实上,经常'新'只使用malloc。
答案 7 :(得分:1)
对称要求的一个例子是使用向量化(SIMD)指令时。 (它可以在没有对齐的情况下使用,但如果你使用一种需要对齐的指令,它会更快。)
答案 8 :(得分:1)
强制内存对齐在基于RISC的体系结构(如MIPS)中更为常见
这些类型的处理器AFAIK的主要思想实际上是速度问题
RISC方法论的全部内容是拥有一组简单快速的指令(通常每条指令一个存储周期)。这并不一定意味着它具有比CISC处理器更少的指令,更多的是它具有更简单,更快速的指令
许多MIPS处理器,虽然8字节可寻址将是字对齐的(通常是32位但不总是),然后掩盖相应的位。
这个想法是,这比执行未对齐的加载更快地执行对齐的加载+位掩码。
通常(当然这实际上取决于芯片组),执行未对齐的负载会产生总线错误,因此RISC处理器将提供“未对齐的加载/存储”指令,但这通常比相应的对齐加载/存储慢得多。
当然,这仍然没有回答他们为什么这样做的问题,即记忆词对齐给你的优势是什么? 我不是硬件专家,我相信这里有人可以给出更好的答案,但我最好的两个猜测是: 1.当字对齐时从高速缓存中取出可以快得多,因为许多高速缓存被组织成高速缓存行(8到512字节之间),而高速缓存通常比RAM贵得多,你想要做得最多它 2.访问每个存储器地址可能要快得多,因为它允许您通读“突发模式”(即在需要之前获取下一个连续地址)
请注意,对于非对齐商店,以上都不是绝对不可能的,我猜测(虽然我不知道)很多都归结为硬件设计选择和成本