我读过很多关于内存排序的文章,他们都只说CPU会重新排序加载和存储。
CPU(我对x86 CPU特别感兴趣)是否只对加载和存储进行重新排序,而不对其拥有的其余指令进行重新排序?
无序处理器通常可以在可能、可行、有利于性能的情况下对所有指令重新排序。由于注册重命名,这对机器代码来说是透明的,除了加载和存储的情况,这就是为什么人们通常只谈论加载和存储重新排序,因为这是唯一可以观察到的重新排序。
通常,FPU异常也是您可以观察到重新排序的地方。出于这个原因,大多数乱序处理器都有不精确的异常,但x86除外。在x86上,处理器确保报告异常时就好像浮点运算没有重新排序一样。
无序执行保留了在单线程/内核中按程序顺序运行的假象。这就像C/C的“仿佛优化”规则:在内部做任何你想做的事情,只要可见效果相同。
单独的线程只能通过内存相互通信,因此内存操作(加载/存储)的全局顺序是执行1的唯一外部可见副作用。
即使按顺序,CPU的内存操作也会变得无序,全局可见。(例如,即使是带有存储缓冲区的简单RISC管道也会有存储负载重新排序,如x86)。如果CPU没有特别避免加载/存储,它可以按顺序启动加载/存储,但允许加载/存储按顺序完成(以隐藏缓存未命中延迟),也可以重新排序加载(或者像现代x86一样,严格按顺序执行,但通过仔细跟踪内存排序,假装没有按顺序执行)。
一个简单的例子:两个ALU依赖链可以重叠
(相关:http://blog.stuffedcow.net/2013/05/measuring-rob-capacity/有关查找指令级并行性的窗口有多大的更多信息,例如,如果您将其增加到乘以200
,您会看到只有有限的重叠。还相关:这个初学者到中级的答案我写了关于像Haswell或Skylake这样的OoO CPU如何查找和利用ILP。)
另请参阅现代微处理器90分钟指南!了解出色的超标量和乱序执行CPU。
要在此处更深入地分析lford
的影响,请参阅了解lford对具有两个长依赖链的循环的影响,以增加长度
global _start
_start:
mov ecx, 10000000
.loop:
times 25 imul eax,eax ; expands to imul eax,eax / imul eax,eax / ...
; lfence
times 25 imul edx,edx
; lfence
dec ecx
jnz .loop
xor edi,edi
mov eax,231
syscall ; sys_exit_group(0)
它在x86-64 Linux上构建(使用nasm-ld)为静态可执行文件,在Skylake上,每链25*10M imul指令的预期750M时钟周期乘以3个周期延迟。
注释掉其中一个imul
链不会改变运行所需的时间:仍然750M周期。
否则,这就是两个依赖链交错无序执行的明确证明。(<代码>imul吞吐量为1个时钟,延迟为3个时钟。http://agner.org/optimize/.因此,第三条依赖链可以在没有太大减速的情况下混合进来)。
任务集c 3 ocperf中的实际数字。py stat—没有大的num-etask时钟、上下文切换、cpu迁移、页面错误、周期:u、分支:u、指令:u、发出的uops\u。任何:u,uops\u已执行。线程:u,uops\u失效。retire\u插槽:u-r3/imul标准:
750566384-0.1%
(lford
序列化指令执行,但不序列化内存存储)。如果您不使用来自WC内存的NT加载(这不会意外发生),除了停止稍后的指令执行,直到之前的指令“本地完成”。即直到它们从乱序核心中退出。这可能就是它将总时间增加一倍以上的原因:它必须等待块中的最后一个imul
才能通过更多管道阶段。)
英特尔上的lford总是这样,但在AMD上,它只是部分序列化,启用了Spectre缓解。
脚注1:当两个逻辑线程共享一个物理线程(超线程或其他SMT)时,也有定时侧通道。例如,如果另一个超线程不需要端口1执行任何操作,则在最近的Intel CPU上执行一系列独立的imul
指令将以每时钟1运行。因此,您可以通过在一次逻辑内核上对ALU绑定循环进行计时来测量端口0压力的大小。
其他微体系结构侧通道(如缓存访问)更可靠。例如,Spectre/Meldown最容易利用缓存读取端通道而不是ALU进行攻击。
但是,与体系结构支持的对共享内存的读/写相比,所有这些侧通道都很挑剔且不可靠,因此它们只与安全相关。它们不是故意在同一个程序中用于线程之间的通信。
Skylake上的mfence意外地阻止了imul的无序执行,就像lfence一样,尽管没有记录有这种效果。(有关更多信息,请参阅“移动到聊天室”讨论)。
xchg[rdi],ebx
(隐式lock
前缀)根本不会阻止ALU指令的乱序执行。在上述测试中,将lford
替换为xchg
或lock
ed指令时,总时间仍然是750M个周期。
但使用mfence,2条mfence指令的周期成本高达1500M。为了做一个对照实验,我保持指令计数不变,但将mfence指令相邻移动,这样imul链就可以相互重新排序,而2条mfence指令的时间降到了750M。
这种Skylake行为很可能是微码更新修复勘误表SKL079的结果,WC内存中的MOVNTDQA可能传递早期MFENCE指令。勘误表的存在表明,过去可以在mfence完成之前执行后续指令,因此他们可能会对mfence的微码进行强力修复,将lfence UOP添加到微码中。
这是另一个有利于将xchg用于seq cst存储的因素,甚至可以将锁添加到一些堆栈内存中作为独立的屏障。Linux已经做到了这两件事,但编译器仍然使用mfence作为屏障。查看为什么具有顺序一致性的std::原子存储使用XCHG?
(另请参阅Google Groups线程上关于Linux屏障选择的讨论,其中有3条单独的建议链接,建议使用锁定地址0,-4(%esp/rsp)而不是将mfence作为独立屏障。
ARM允许使用后续存储重新排序加载,以便使用以下伪代码: 可能会导致temp0==temp1==1(在实践中也可以观察到)。我很难理解这是怎么发生的;按顺序提交似乎会阻止它(据我所知,几乎所有OOO处理器都存在这种情况)。我的推理是“负载在提交之前必须有它的值,它在存储之前提交,并且存储的值在提交之前不能对其他处理器可见。” 我猜我的一个假设一定是错的,类似以下的假设一定成立: > 负载可以在其值
问题内容: 我只是碰到一篇文章,声称我从未听过,也找不到其他地方。声称是从另一个线程的角度来看,可以根据构造函数内部的指令对构造函数返回的值的分配进行重新排序。换句话说,声称是在下面的代码中,另一个线程可以读取其中未设置的非空值。 这是真的? 编辑: 我认为从线程执行的角度来看,可以保证的分配与的分配具有先发生后关系。但是,这两个值都可能缓存在寄存器中,并且可能未按照最初写入的顺序将它们刷新到主存
问题内容: 我正在阅读此博客文章。 作者正在谈论在多线程环境中打破in 。 有了: 变成: 作者说,我引用: “我在这里所做的是添加一个附加读取: 哈希的第二次读取,在返回之前 。听起来很奇怪,而且不太可能发生,第一次读取可以返回正确计算出的哈希值,内存模型允许第二次读取返回0!这是允许的,因为该模型允许对操作进行广泛的重新排序。第二次读取实际上可以在代码中移动,以便处理器在第一次读取之前进行处理
变量res的值应等于3。但是当我打开优化时,编译器错误地重新排列了指令,并且res包含一些垃圾。一些可能的重新排序示例: 这是编译器中的错误吗?还是不允许像这样访问结构数据成员? 编辑: 我刚刚意识到之前的代码实际上有效,抱歉。但这不起作用: 当编译时不知道变量i时,编译器会错误地重新排序指令。
问题内容: 我在伪操作环境中工作,我们在收到数据后制作新图像。有时,当输入新数据时,我们需要重新打开图像并更新该图像以创建合成,添加叠加等。除了添加到图像之外,还需要修改标题,图例等。 matplotlib中有内置的东西可以让我存储和重新加载matplotlib.pyplot对象以供以后使用吗?它需要保持对所有相关对象(包括图形,线条,图例等)的访问。也许咸菜是我想要的,但我对此表示怀疑。 问题答
问题内容: 在《 Java Concurrency InPractice》一书中,有几次告诉我们可以通过编译器,运行时JVM甚至处理器来重新排序程序的指令。因此,我们应该假定执行的程序不会以与源代码中指定的顺序完全相同的顺序执行其指令。 但是,上一章讨论的Java内存模型提供了一系列先 发生后 规则的清单,这些规则指示JVM保留哪些指令顺序。这些规则中的第一个是: “程序顺序规则。线程中的每个动作