为什么在x86上自然alignment的variablesprimefaces上的整数赋值?

我一直在阅读这篇关于primefaces操作的文章 ,它提到了x86上的32位整数赋值是primefaces的,只要variables是自然alignment的。

为什么自然alignment确保primefaces性?

“自然”alignment意味着alignment到自己的宽度。 因此,加载/存储将永远不会跨越任何types的边界(比如页面,caching行或用于不同caching之间的数据传输的更小的块大小)进行分割。


首先,这假定int是用单个存储指令更新的,而不是单独写入不同的字节。 这是std::atomic保证的一部分,但是纯C或C ++不。 但通常情况是这样。 x86-64系统V ABI不禁止编译器访问非primefaces的intvariables,尽pipe它要求int的默认alignment方式为4B。

数据竞赛在C和C ++中都是未定义行为,因此编译器可以并且假设内存不是asynchronous修改的。 对于保证不中断的代码,请使用C11 stdatomic或C ++ 11 std :: atomic 。 否则,编译器只会在寄存器中保留一个值,而不是每次读取它时重新加载

 std::atomic<int> shared; // shared variable (in aligned memory) int x; // local variable (compiler can keep it in a register) x = shared.load(std::memory_order_relaxed); shared.store(x, std::memory_order_relaxed); // shared = x; // don't do that unless you actually need seq_cst, because MFENCE is much slower than a simple store 

因此,我们只需要讨论像mov [shared], eax这样的insn的行为。


TL; DR:x86 ISA确保自然alignment的存储和加载是primefaces的,高达64位宽。 所以编译器只要确保std::atomic<T>具有自然alignment,就可以使用普通的存储/加载。

(但是请注意,i386 gcc -m32无法为C11 _Atomic 64位types做这些工作,只能将它们对准到4B,所以atomic_llong实际上并不是primefaces的atomic_llong = 65146#c4 )。 g++ -m32std::atomic是好的,至less在g ++ 5中是因为https://gcc.gnu.org/bugzilla/show_bug.cgi?id=65147在2015年被修改为;<atomic>头。 虽然这并没有改变C11的行为。)


IIRC,有SMP 386系统,但是当前的内存语义直到486才build立。这就是为什么手册说“486和更新”。

从“英特尔®64和IA-32架构软件开发人员手册,第3卷”中, 我的笔记以斜体表示 。 (另请参阅x86标签维基链接:所有卷的当前版本 ,或直接链接到2015年12月vol3 pdf的第256页 )

在x86术语中,“字”是两个8位字节。 32位是双字,或DWORD。

第8.1.1节保证的primefaces操作

Intel486处理器(以及更新的处理器)保证了以下基本的内存操作将始终以primefaces方式进行:

  • 读或写一个字节
  • 读取或写入在16位边界上alignment的单词
  • 读取或写入在32位边界上alignment的双字 (这是“自然alignment”的另一种方式)

粗体的最后一点是你的问题的答案:这种行为是处理器是x86 CPU(即ISA的实现)所需要的一部分。


本节的其余部分为更新的Intel CPU提供进一步保证:Pentium将此保证扩大到64位。

奔腾处理器(以及更新的处理器)确保以下附加的内存操作始终以primefaces方式执行:

  • 读取或写入在64位边界上alignment的四字(例如,x87加载/存储cmpxchg8bcmpxchg8b (在Pentium P5中是新的))
  • 16位访问适用于32位数据总线的未caching内存位置。

本节继续指出跨caching行(和页面边界)分割的访问不保证是primefaces的,并且:

“访问大于四字的数据的x87指令或SSE指令可以使用多个存储器访问来实现。

AMD公司的手册与英特尔公司关于alignment的64位和更窄的加载/存储是primefaces的一致

因此,如果数据alignment,则64位x87和MMX / SSE加载/存储到64b(例如, movqmovsdmovhpspinsrqextractps等) primefaces的。 gcc -m32使用movq xmm, [mem]来实现std::atomic<int64_t>类的primefaces64位加载。 Clang4.0 -m32不幸地使用了lock cmpxchg8b 错误33109 。

在一些具有128b或256b内部数据path(执行单元和L1之间,以及不同高速caching之间)的CPU上,128b甚至256bvector加载/存储是primefaces的,但这不能由任何标准保证或在运行时容易查询, 不幸的是编译器实现std::atomic<__int128>或16B结构 。

如果要在所有x86系统上使用primefaces128b,则必须使用lock cmpxchg16b (仅在64位模式下可用)。 (在第一代的x86-64处理器中是不可用的,你需要用-mcx16和gcc / clang 来发射它们 。)

甚至在内部执行primefaces128b加载/存储的CPU也可以在多插槽系统中显示非primefaces行为,其中一致性协议以较小的块运行:例如AMD Opteron 2435(K10),其中线程运行在单独的套接字上,并与HyperTransport连接 。


英特尔和AMD的手册不一致地访问caching内存 。 所有x86 CPU的通用子集是AMD规则。 可高速caching意味着回写或直写内存区域,而不是用PAT或MTRR区域设置的不可caching或写入组合。 这并不意味着caching行必须在L1caching中已经很热。

  • 英特尔P6和更高版本保证可高速caching的加载/存储的primefaces性,只要它们位于单个caching行(64B或32B,在像PentiumIII这样的非常旧的CPU上),就可以加载/存储多达64位。
  • AMD保证适合一个8Balignment块的可caching加载/存储的primefaces性。 这是有道理的,因为我们从多路Opteron上的16B存储testing中知道,HyperTransport只能在8B块中传输,而不能在传输时locking以防撕裂。 (往上看)。 我想lock cmpxchg16b必须专门处理。

    可能相关:AMD使用MOESI直接在不同内核的caching之间共享脏caching行,所以一个内核可以从其caching行的有效副本中读取数据,而对另一个caching中的数据进行更新。

    英特尔使用MESIF ,它需要肮脏的数据传播出去的大共享包容性三级caching作为一致性stream量的后盾。 L3是标签包含每个核心L2 / L1高速caching,即使对于必须在L3中处于无效状态的线路,因为在每个核心L1高速caching中是M或E. 在Haswell / Skylake中,L3和每核心caching之间的数据path只有32B的宽度,所以它必须缓冲或避免在两个caching行的两个读取之间发生的来自一个核心的L3的写入,这可能导致在32B的边界。

手册的相关部分:

P6系列处理器(以及更新的Intel处理器)确保以下附加内存操作始终以primefaces方式进行:

  • 未alignment的16,32和64位访问高速caching内存,适合caching线。

AMD64手册7.3.2访问primefaces性
在任何处理器模型中,caching,自然alignment的单个加载或高达四字的存储都是primefaces的,因为未alignment的加载或存储小于四字的存储完全包含在自然alignment的四字

请注意,AMD保证小于qword的任何负载的primefaces性,但是仅限于2倍的功耗。 32位保护模式和64位长模式可以将48位m16:32作为内存操作数加载到具有远程call或远程jmp cs:eip 。 (远程调用会将堆栈中的内容推送到内存中)。IDK如果是单个48位访问或单独的16位和32位访问。

已经尝试正式确定x86内存模型,最近的一个是2009年的x86-TSO(扩展版)论文 (链接来自x86标记wiki的内存sorting部分)。 因为它们定义了一些用自己的符号来expression事物的符号,所以没有用到可以滑动的东西,而且我也没有尝试去真正阅读它。 IDK如果它描述了primefaces性规则,或者它只关心内存sorting


primefaces读取 – 修改 – 写入

我提到cmpxchg8b ,但是我只是在谈论负载和每个分别是primefaces的商店(即没有“撕裂”,其中一半的负载来自一个商店,另一半的负载来自不同的商店)。

为了防止在加载和存储之间修改那个内存位置的内容,你需要lock cmpxchg8b ,就像你需要把整个read-modify-write的lock inc [mem]primefaces化。 还要注意的是,即使cmpxchg8b没有lock也会执行一个primefaces加载(也可能是一个存储),但是通常将它用作期望=期望的64b加载并不安全。 如果内存中的值正好符合您的预期,那么您将获得该位置的非primefaces读取 – 修改 – 写入。

lock前缀甚至可以使未alignment的访问跨越caching行或页面边界primefaces,但是您不能在mov使用它来创build未alignment的存储或加载primefaces。 它只能用于内存目标读取 – 修改 – 写入指令,如add [mem], eax

lockxchg reg, [mem]是隐含的,所以不要使用mem和mem来保存代码大小或指令数量,除非性能不相关,只有当你想要内存屏障和/或primefaces交换时才使用它。当代码大小是唯一重要的事情,例如在引导扇区。)

另请参见: 可以num ++为'int num'的primefaces吗?


为什么lock mov [mem], reg对primefaces未alignment的存储区不存在

从insn ref手册(Intel x86 manual vol2), cmpxchg

该指令可以与LOCK前缀一起使用,以允许指令以primefaces方式执行。 为了简化到处理器总线的接口,目标操作数接收一个写周期而不考虑比较结果。 如果比较失败,则返回目标操作数; 否则,源操作数被写入目的地。 ( 处理器不会产生locking的读取,也不会产生locking的写入 。)

内存控制器内置于CPU之前,此devise决策降低了芯片组的复杂性。 它仍然可以在MMIO区域上执行PCI-Express总线而不是DRAM。 对于lock mov reg, [MMIO_PORT]产生一个写入以及对存储器映射的I / O寄存器的一个读操作,会造成混淆。

另一种解释是确保你的数据是自然alignment的并不是很难,而lock store会比只保证你的数据是alignment的更糟。 把晶体pipe花在那些速度太慢,不值得使用的东西上是很愚蠢的。 如果你真的需要它(也不介意读取内存),你可以使用xchg [mem], reg (XCHG有一个隐含的LOCK前缀),它甚至比假想的lock mov慢。

使用lock前缀也是一个完整的内存屏障,所以它强加了一个超过primefacesRMW的性能开销。 (有趣的事实:在mfence存在之前,一个常见的习惯用法是lock add [esp], 0 ,除了clobbering flags和做locking操作之外,它是no-op。 [esp]在L1 cache中几乎总是很热,导致与任何其他核心的争夺,这个成语可能仍然比在AMD CPU上的MFENCE更有效率。)


这个devise决定的动机:

没有它,软件将不得不使用1字节的锁(或某种可用的primefacestypes)来防止对32位整数的访问,与共享primefaces读取访问相比,这是非常低效的,例如由定时器中断更新的全局时间戳variables。 它可能基本上是免费的硅,以保证alignment访问总线宽度或更小。

为了locking可能,需要某种primefaces访问。 (实际上,我猜硬件可以提供某种完全不同的硬件辅助locking机制。)对于在其外部数据总线上进行32位传输的CPU,将其作为primefaces单位是合理的。


既然你提供了一个赏金,我假设你正在寻找漫长的回答,所有有趣的方面主题。 让我知道,如果有什么我没有覆盖,你认为这将使这个问答对未来的读者更有价值。

既然你链接了一个问题 , 我强烈推荐阅读更多Jeff Preshing的博客文章 。 它们非常出色,帮助我将所了解的内容组合在一起,以理解C / C ++源代码中的内存sorting和不同硬件体系结构的asm,以及如何/何时告诉编译器如果不需要,直接写asm。

如果一个32位或更小的对象在内存的“正常”部分中自然alignment,则80386或80386sx以外的任何80386或兼容处理器都可以在单个操作中读取或写入对象的所有32位。 虽然平台以快速和有用的方式做某些事情的能力并不一定意味着平台有时候会出于某种原因以某种其他方式来做,而且我相信在很多(如果不是全部的话)x86处理器上,有区域的内存,一次只能访问8或16位,我不认为英特尔曾经定义的任何情况下,请求一个alignment的32位访问“正常”的内存区域将导致系统读取或者写下部分价值,而不去读写整个东西,我不认为英特尔有任何意图为“正常”的记忆区域定义任何这样的东西。

自然alignment意味着该types的地址是types大小的倍数。

例如,一个字节可以在任何地址,一个短的(假设16位)必须是2的倍数,一个int(假设是32位)必须是4的倍数,必须是长的(假设是64位)是8的倍数。

如果你访问的数据不是自然alignment的,那么CPU会引发一个错误,或者读写内存,而不是primefaces操作。 CPU所采取的行动将取决于架构。

例如,我们已经得到了下面的内存布局的图像:

 01234567 ...XXXX. 

 int *data = (int*)3; 

当我们尝试读取*data ,组成该值的字节被分散在2个int大小的块中,1个字节在块0-3中,3个字节在块4-7中。 现在,只是因为块在逻辑上彼此相邻,并不意味着它们是物理上的。 例如,块0-3可以位于cpucaching行的末尾,而块3-7位于页面文件中。 当CPU去访问块3-7,以获得它所需要的3个字节时,可能会发现该块不在内存中,并表示需要内存页面。这可能会阻止调用进程,而操作系统将内存重新分页。

在内存被分页之后,在你的进程被唤醒之前,另外一个可能会出现,并且写入一个Y来解决4.然后你的进程被重新安排,CPU完成读取,但是现在它已经读取了XYXX,而不是您期望的XXXX。

如果你问为什么这样devise的话,我认为这是CPU架构devise的一个好的副产品。

早在486年,就没有多核CPU或QPI链接,所以当时的primefaces性并不是一个严格的要求(DMA可能要求它?)。

在x86上,数据宽度为32位(x86_64为64位),这意味着CPU可以一次读写数据宽度。 内存数据总线通常与此数字相同或更宽。 结合对准地址的读/写一次完成的事实,自然没有任何东西阻止读/写是不primefaces的。 你在同一时间获得速度/primefaces。

要回答你的第一个问题,一个variables是自然alignment的,如果它存在于一个内存地址的大小的倍数。

如果我们只考虑 – 就像你所链接的文章所做的那样 – 分配指令 ,那么alignment保证了primefaces性,因为MOV(赋值指令)在alignment的数据上是deviseprimefaces的。

其他types的指令,例如INC,需要被locking (一个x86前缀,在前缀操作的持续时间内给予当前处理器独占访问共享内存),即使数据是alignment的,因为它们实际上是通过多个步骤(=指令,即负载,公司,商店)。