单个进程中的两个不同线程可以通过读取和/或写入来共享公共存储器位置.
通常,这种(有意)共享是使用lock
x86上的前缀使用原子操作实现的,该前缀对于lock
前缀本身(即,无竞争成本)具有相当广为人知的成本,并且当实际共享高速缓存行时还具有额外的一致性成本(真或假共享).
在这里,我对生产 - 消费者成本感兴趣,其中单个线程P
写入内存位置,另一个线程`C从内存位置读取,都使用普通读取和写入.
在同一个套接字上的不同内核上执行此类操作的延迟和吞吐量是多少,并且在最近的x86内核上在同一物理内核上执行兄弟超线程时进行比较.
在标题中,我使用术语"超级兄弟"来指代在同一核心的两个逻辑线程上运行的两个线程,以及核心间兄弟,以指代在不同物理核心上运行的两个线程的更常见情况.
假设我有两个线程来操纵全局变量x
.每个线程(或我认为的每个核心)都有一个缓存副本x
.
现在说Thread A
执行以下说明:
set x to 5
some other instruction
Run Code Online (Sandbox Code Playgroud)
现在set x to 5
执行时,缓存的值x
将设置为5
,这将导致缓存一致性协议使用新值来操作和更新其他核心的缓存x
.
现在我的问题是:什么时候x
实际设置5
在Thread A
缓存中,其他内核的缓存在some other instruction
执行之前是否会更新?或者应该使用内存屏障来确保?:
set x to 5
memory barrier
some other instruction
Run Code Online (Sandbox Code Playgroud)
注意:假设指令是按顺序执行的,也假设set x to 5
执行时,5
会立即放入线程A的缓存中(因此指令不会放在队列中或稍后要执行的内容).
我研究了Java内存模型并看到了重新排序问题.一个简单的例子:
boolean first = false;
boolean second = false;
void setValues() {
first = true;
second = true;
}
void checkValues() {
while(!second);
assert first;
}
Run Code Online (Sandbox Code Playgroud)
重新排序是非常不可预测和奇怪的.此外,它破坏了抽象.我认为处理器架构必须有充分的理由去做一些对程序员来说太不方便的事情. 这些原因是什么?
有很多关于如何处理重新排序的信息,但我找不到任何关于它为什么需要的信息.在任何地方,人们只会说"这是因为一些性能优势".例如,second
之前存储的性能优势是first
什么?
您能推荐一些关于此的文章,论文或书籍,或者自己解释一下吗?
java optimization multithreading cpu-architecture compiler-optimization
这个问题是对此的跟进/澄清:
MOV x86 指令是否实现了 C++11 memory_order_release 原子存储?
这表明MOV
汇编指令足以在 x86 上执行获取-释放语义。我们不需要LOCK
,围栏xchg
等。但是,我很难理解这是如何工作的。
英特尔文档第 3A 卷第 8 章指出:
https://software.intel.com/sites/default/files/managed/7c/f1/253668-sdm-vol-3a.pdf
在单处理器(核心)系统中......
- 读取不会与其他读取重新排序。
- 写入不会与较旧的读取重新排序。
- 对内存的写入不会与其他写入重新排序,但以下情况除外:
但这是针对单核的。多核部分似乎没有提到如何强制执行负载:
在多处理器系统中,以下排序原则适用:
- 单个处理器使用与单处理器系统相同的排序原则。
- 所有处理器都以相同的顺序观察单个处理器的写入。
- 来自单个处理器的写入与来自其他处理器的写入无关。
- 记忆排序服从因果关系(记忆排序尊重传递可见性)。
- 除了执行存储的处理器之外的处理器以一致的顺序看到任何两个存储
- 锁定指令具有总顺序。
那么如何才能MOV
单独促进获取释放呢?
ARM允许重新排序加载后续存储,以便以下伪代码:
// CPU 0 | // CPU 1
temp0 = x; | temp1 = y;
y = 1; | x = 1;
可以导致temp0 == temp1 == 1
(并且,这在实践中也是可观察到的).我无法理解这是怎么发生的; 似乎有序提交会阻止它(这是我的理解,它存在于几乎所有的OOO处理器中).我的理由是"在提交之前,负载必须具有其值,它在存储之前提交,并且在提交之前,存储的值不会对其他处理器可见."
我猜我的一个假设肯定是错的,并且必须遵循下列之一:
说明不需要提交一路有序.稍后的存储可以安全地提交并在之前的加载之前变得可见,只要在存储提交核心时可以保证先前的加载(以及所有中间指令)不会触发异常,并且加载的地址是保证与商店不同.
负载可以在其值已知之前提交.我不知道如何实现这一点.
商店在提交之前可以显示.也许某个内存缓冲区允许将存储转发到另一个线程的加载,即使负载先前已加入?
还有别的吗?
有许多假设的微体系结构特征可以解释这种行为,但我最好的是那些实际存在于现代弱有序CPU中的那些.
我试图理解英特尔系统编程指南的 8.2节(PDF格式的第3卷).
特别是,我看到两种不同的重新排序方案:
8.2.3.4载荷可以与较早的商店重新排序到不同的地点
和
8.2.3.5允许处理器内转发
但是,我不明白这些场景与可观察效果POW之间的区别.这些部分提供的示例似乎可以互换.8.2.3.4例子可以通过8.2.3.5规则以及它自己的规则来解释.反之亦然,对我来说也是如此,尽管在那种情况下我并不确定.
所以这是我的问题:有没有更好的例子或解释8.2.3.4的可观察效果与8.2.3.5的可观察效果有何不同?
vTune文档将Memory Order Machine Clear性能事件描述为:
当来自另一个处理器的侦听请求与管道中数据操作的源匹配时,将发生内存排序(MO)机器清除。在这种情况下,在撤消正在进行的装载和存储之前,应清理管道。
但是我不明白为什么会这样。在不同逻辑处理器上的加载和存储之间没有同步顺序。
处理器可以假装在所有当前的机上数据操作都提交后进行监听。
此问题也在此处描述
每当CPU内核检测到“内存排序冲突”时,就会触发内存排序机清除。基本上,这意味着一些当前待处理的指令试图访问我们刚刚发现同时写入了其他CPU内核的内存。由于这些指令仍被标记为待处理,而“此存储器刚刚被写入”事件则表示其他某个内核已成功完成写入,因此,待处理指令以及取决于结果的所有内容都是错误的:当我们开始执行这些指令时在说明中,我们使用的内存内容版本已过时。因此,我们需要把所有工作都扔掉,然后再做完。这很清楚。
但这对我来说没有任何意义,CPU不需要重新执行Load-Queue中的装载,因为没有针对非锁定装载/存储的总订单。
我可以看到一个问题,即允许对负载进行重新排序:
;foo is 0
mov eax, [foo] ;inst 1
mov ebx, [foo] ;inst 2
mov ecx, [foo] ;inst 3
Run Code Online (Sandbox Code Playgroud)
如果执行顺序为1 3 2,则mov [foo], 1
3至2之间的存储会导致
eax = 0
ebx = 1
ecx = 0
Run Code Online (Sandbox Code Playgroud)
这确实违反了内存排序规则。
但是负载不能随负载重新排序,那么当来自另一个内核的监听请求与任何飞行负载的来源相匹配时,为什么英特尔的CPU会刷新管道?
此行为可以防止什么错误情况?
因此还有很多问题,例如https://mirrors.edge.kernel.org/pub/linux/kernel/people/paulmck/perfbook/perfbook.2018.12.08a.pdf和Preshing的文章如https:/ /preshing.com/20120710/memory-barriers-are-like-source-control-operations/及其整个系列文章就不同的障碍类型提供的排序和可见性保证方面抽象地讨论了内存排序。我的问题是,如何在x86和ARM微体系结构上实现这些障碍和内存排序语义?
对于商店-商店壁垒,好像在x86上,商店缓冲区保持商店的程序顺序并将它们提交到L1D(因此使它们以相同的顺序在全局可见)。如果存储缓冲区未排序,即未按程序顺序维护它们,那么如何实现存储障碍?它只是以这样的方式“标记”存储缓冲区,即在屏障提交之前将存储提交到缓存一致性域,然后在屏障之后提交?还是存储屏障实际上刷新了存储缓冲区并暂停了所有指令,直到刷新完成?可以同时实现吗?
对于负载障碍,如何防止负载重新排序?很难相信x86将按顺序执行所有加载!我假设加载可以乱序执行,但是可以按顺序提交/退出。如果是这样,如果一个cpu在2个不同的位置执行2次加载,那么一个加载如何确保它从T100中得到一个值,而下一个加载在T100上或之后得到它?如果第一个负载未命中高速缓存并正在等待数据,而第二个负载命中并获取其值,该怎么办。当负载1获得其值时,如何确保它获得的值不是来自该负载2的值的较新商店?如果负载可以无序执行,如何检测到违反内存排序的情况?
类似地,如何实现负载存储屏障(在x86的所有负载中都是隐含的)以及如何实现存储负载屏障(例如mfence)?即dmb ld / st和dmb指令在ARM上是如何微体系结构的?每个负载和每个存储区以及mfence指令在x86上如何进行微体系结构,以确保内存排序?
x86 x86-64 cpu-architecture memory-barriers micro-architecture
我以为我了解L1D写未命中是如何处理的,但仔细想想却让我感到困惑。
这是一个汇编语言片段:
;rdi contains some valid 64-bytes aligned pointer
;rsi contains some data
mov [rdi], rsi
mov [rdi + 0x40], rsi
mov [rdi + 0x20], rsi
Run Code Online (Sandbox Code Playgroud)
假设[rdi]
和[rdi + 0x40]
行在 l1d 中不处于 Exclusive 或 Modified 状态。然后我可以想象以下动作序列:
mov [rdi], rsi
退休。mov [rdi], rsi
尝试将数据写入 l1d。RFO 启动,数据放入 WC 缓冲区。mov [rdi + 0x40], rsi
退休(mov [rdi], rsi
已经退休,所以有可能)mov [rdi + 0x40], rsi
为连续的缓存行启动 RFO,数据被放入 WC 缓冲区。mov [rdi + 0x20], rsi
退休(mov [rdi …
当无序处理器遇到类似的东西时
LOAD R1, 0x1337
LOAD R2, $R1
LOAD R3, 0x42
Run Code Online (Sandbox Code Playgroud)
假设所有访问都会导致高速缓存未命中,处理器是否可以在询问内容$ R1甚至0x1337之前询问内存控制器0x42的内容?如果是这样,假设访问$ R1将导致异常(例如,分段错误),我们可以认为0x42是以推测方式加载的,对吗?
顺便说一句,当一个加载存储单元向内存控制器发送请求时,它是否可以在收到前一个请求之前发送第二个请求?
我的问题并不针对任何架构.欢迎与任何主流架构相关的答案.
x86 ×7
assembly ×3
arm ×2
concurrency ×2
cpu-cache ×2
x86-64 ×2
architecture ×1
c++ ×1
intel ×1
java ×1
memory ×1
memory-model ×1
optimization ×1
performance ×1
powerpc ×1
rfo ×1
sparc ×1
stdatomic ×1