处理器在高速缓存一致性操作期间是否停止
Does processor stall during cache coherence operation
假设变量 a = 0
Processor1: a = 1
Processor2: print(a)
处理器 1 首先执行它的指令,然后在下一个周期处理器 2 读取变量来打印它。同样是:
处理器 2 会停止,直到缓存一致性操作完成,它会打印 1
P1: |--a=1--|---cache--coherence---|----------------
P2: ------|stalls due to coherence-|--print(a=1)---|
time: ----------------------------------------------->
处理器 2 将在高速缓存一致性操作完成之前运行,并且在此之前它的内存视图将过时。所以它会打印 0?
P1: |--a=1--|---cache--coherence---|
P2: ----------|---print(a=0)---|----
time: ------------------------------->
换句话说,在缓存一致性操作完成之前,处理器是否可以查看陈旧的内存?
本例中对 a
的读写访问是并发的,它们可以按任何顺序完成。这取决于哪个处理器首先访问该行。高速缓存一致性仅保证同一一致性域中的所有处理器都同意存储在所有高速缓存行中的值。所以最终的结果不可能是a
有两份,一份的值为0,一份为1。
如果要确保processor2 看到processor1 写入的值,则必须使用同步机制。一个简单但低效的方法是:
Processor1:
a = 1
rel = 1
Processor2:
while(rel != 1){ }
print(a)
如果满足以下属性,此方法有效:
- 存储在编译器级别和 ISA 级别均按顺序完成。
- 加载在编译器级别和 ISA 级别均按顺序完成。
满足这些属性的 ISA 的一个例子是 x86-64,假设 rel
不大于 8 个字节并且自然对齐并且所有变量都不是从 WC 内存类型的内存区域分配的.
关于您对问题的更新。如果 processor1 在 processor2 读取之前获得了该行的所有权,则 processor2 可能会停止,直到 processor1 完成其写入操作并获得更新的行。如果 Processor1 检测到来自另一个处理器的对该行的读取请求,它可能会决定在写入之前放弃对该行的所有权,但必须以不会发生活锁的方式执行。这实际上是活锁如何在一致性中发生的标准示例。根据 Intel 的规范更新文档,在 Intel Pentium 4 处理器中,由于 RFO 请求分配的行在至少被访问一次之前不会被驱逐,正是为了防止活锁的发生。这也解释了为什么对于尚未退休的 WB 商店支持投机性 RFO 并不容易。
所有现代 ISA 使用(变体)MESI 来实现高速缓存一致性。这始终保持所有处理器具有的内存共享视图(通过缓存)的一致性。
参见示例 Can I force cache coherency on a multicore x86 CPU? 这是一个常见的误解,认为存储进入缓存而其他核心仍然具有缓存行的旧副本,然后 "cache coherence" 必须发生。
但事实并非如此:要修改缓存行,CPU 需要对该行具有 独占 所有权(MESI 的修改或独占状态)。这只有在收到对 Read For Ownership 的响应后才有可能,该响应使缓存行的所有其他副本无效,如果它之前处于 Shared 或 Invalid 状态。例如,参见 。
但是,内存模型允许对存储和加载进行本地重新排序。顺序一致性会太慢,因此 CPUs 总是至少允许 StoreLoad 重新排序。有关 x86 上使用的 TSO(总存储顺序)内存模型的大量详细信息,另请参阅 。许多其他 ISA 使用更弱的模型。
在这种情况下,对于未同步的 reader,如果两者都 运行 在不同的核心上,则存在三种可能性
load(a)
在缓存行失效之前发生在 core#2 上,因此它读取旧值,因此有效地发生在全局顺序中的 a=1
存储之前。 加载可以命中一级缓存。
load(a)
发生在 core#1 将存储提交到其 L1d 缓存并且尚未回写之后。 Core#2 的读取请求触发 Core#2 写回共享缓存级别(例如 L3),并将该行置于共享状态。 L1d. 肯定会漏载
load(a)
发生在回写到内存之后,或者至少 L3 已经发生,因此它不必等待 core#1 回写。 加载将在 L1d 中丢失,除非硬件预取出于某种原因将其重新加载。但通常这只会作为顺序访问的一部分发生(例如对数组)。
所以是的,如果另一个核心在该核心尝试加载它之前已经将其提交到缓存,则加载将停止。
另请参阅 以了解有关存储缓冲区对所有内容(包括内存重新排序)的影响的更多信息。
这里没关系,因为你有一个只写的生产者和一个只读的消费者。生产者核心在继续之前不会等待它的商店变得全局可见,并且它可以在它变得全局可见之前立即看到它自己的商店。当您让每个线程查看另一个线程完成的存储时,确实很重要;那么您需要障碍或顺序一致的原子操作(编译器使用障碍实现)。 见https://preshing.com/20120515/memory-reordering-caught-in-the-act
另请参阅
原子RMW如何与MESI一起工作,这对理解这个概念很有帮助。 (例如,原子 RMW 可以通过让核心挂在修改状态的高速缓存行上来工作,并延迟响应 RFO 或请求共享它,直到 RMW 的写入部分已提交。)
假设变量 a = 0
Processor1: a = 1
Processor2: print(a)
处理器 1 首先执行它的指令,然后在下一个周期处理器 2 读取变量来打印它。同样是:
处理器 2 会停止,直到缓存一致性操作完成,它会打印 1
P1: |--a=1--|---cache--coherence---|---------------- P2: ------|stalls due to coherence-|--print(a=1)---| time: ----------------------------------------------->
处理器 2 将在高速缓存一致性操作完成之前运行,并且在此之前它的内存视图将过时。所以它会打印 0?
P1: |--a=1--|---cache--coherence---| P2: ----------|---print(a=0)---|---- time: ------------------------------->
换句话说,在缓存一致性操作完成之前,处理器是否可以查看陈旧的内存?
本例中对 a
的读写访问是并发的,它们可以按任何顺序完成。这取决于哪个处理器首先访问该行。高速缓存一致性仅保证同一一致性域中的所有处理器都同意存储在所有高速缓存行中的值。所以最终的结果不可能是a
有两份,一份的值为0,一份为1。
如果要确保processor2 看到processor1 写入的值,则必须使用同步机制。一个简单但低效的方法是:
Processor1:
a = 1
rel = 1
Processor2:
while(rel != 1){ }
print(a)
如果满足以下属性,此方法有效:
- 存储在编译器级别和 ISA 级别均按顺序完成。
- 加载在编译器级别和 ISA 级别均按顺序完成。
满足这些属性的 ISA 的一个例子是 x86-64,假设 rel
不大于 8 个字节并且自然对齐并且所有变量都不是从 WC 内存类型的内存区域分配的.
关于您对问题的更新。如果 processor1 在 processor2 读取之前获得了该行的所有权,则 processor2 可能会停止,直到 processor1 完成其写入操作并获得更新的行。如果 Processor1 检测到来自另一个处理器的对该行的读取请求,它可能会决定在写入之前放弃对该行的所有权,但必须以不会发生活锁的方式执行。这实际上是活锁如何在一致性中发生的标准示例。根据 Intel 的规范更新文档,在 Intel Pentium 4 处理器中,由于 RFO 请求分配的行在至少被访问一次之前不会被驱逐,正是为了防止活锁的发生。这也解释了为什么对于尚未退休的 WB 商店支持投机性 RFO 并不容易。
所有现代 ISA 使用(变体)MESI 来实现高速缓存一致性。这始终保持所有处理器具有的内存共享视图(通过缓存)的一致性。
参见示例 Can I force cache coherency on a multicore x86 CPU? 这是一个常见的误解,认为存储进入缓存而其他核心仍然具有缓存行的旧副本,然后 "cache coherence" 必须发生。
但事实并非如此:要修改缓存行,CPU 需要对该行具有 独占 所有权(MESI 的修改或独占状态)。这只有在收到对 Read For Ownership 的响应后才有可能,该响应使缓存行的所有其他副本无效,如果它之前处于 Shared 或 Invalid 状态。例如,参见
但是,内存模型允许对存储和加载进行本地重新排序。顺序一致性会太慢,因此 CPUs 总是至少允许 StoreLoad 重新排序。有关 x86 上使用的 TSO(总存储顺序)内存模型的大量详细信息,另请参阅
在这种情况下,对于未同步的 reader,如果两者都 运行 在不同的核心上,则存在三种可能性
load(a)
在缓存行失效之前发生在 core#2 上,因此它读取旧值,因此有效地发生在全局顺序中的a=1
存储之前。 加载可以命中一级缓存。load(a)
发生在 core#1 将存储提交到其 L1d 缓存并且尚未回写之后。 Core#2 的读取请求触发 Core#2 写回共享缓存级别(例如 L3),并将该行置于共享状态。 L1d. 肯定会漏载
load(a)
发生在回写到内存之后,或者至少 L3 已经发生,因此它不必等待 core#1 回写。 加载将在 L1d 中丢失,除非硬件预取出于某种原因将其重新加载。但通常这只会作为顺序访问的一部分发生(例如对数组)。
所以是的,如果另一个核心在该核心尝试加载它之前已经将其提交到缓存,则加载将停止。
另请参阅
这里没关系,因为你有一个只写的生产者和一个只读的消费者。生产者核心在继续之前不会等待它的商店变得全局可见,并且它可以在它变得全局可见之前立即看到它自己的商店。当您让每个线程查看另一个线程完成的存储时,确实很重要;那么您需要障碍或顺序一致的原子操作(编译器使用障碍实现)。 见https://preshing.com/20120515/memory-reordering-caught-in-the-act
另请参阅