全局不可见的加载指令

8

由于存储加载转发,一些负载指令是否永远不会全局可见?换句话说,如果加载指令从存储缓冲区获取其值,则它永远不必从高速缓存中读取。


通常情况下,当一个加载指令从L1D缓存中读取时,它是全局可见的。而那些不从L1D中读取的指令应该使其在全局范围内不可见。


1
你可能需要澄清一下你所说的“全局可见性”,并/或者澄清是否有任何潜在的问题促使了这个问题。例如,如果你解释一下全局可见负载和不可见负载之间的区别对回答者会有所帮助。 - BeeOnRope
2
可能有用的是要知道像全局可见性(例如,存储器)这样的术语是有帮助的抽象,以理解体系结构的有序性和可见性属性,但在uarch级别甚至在电学上,许多在概念上全局可见的操作实际上从未对任何其他代理变得可见。例如,考虑一个CPU获取M状态的高速缓存行,并在最终放弃该行之前进行许多写入和读取。除了影响缓存行最终状态的存储之外,这些访问都不会被其他任何代理看到... - BeeOnRope
1
这是我所参考的帖子:[https://dev59.com/N5jga4cB1Zd3GeqPJF_P#38630815]。 - joz
1
谢谢 Joz!通常最好包含引起你有另一个问题的答案引用,这样响应者可以有一些上下文。我现在明白是什么促使了这个问题。我认为 Peter 只是省略了讨论 SLF,因为问题是关于 L1 的,但我认为我会写这两个句子而不涉及 load 可见性,类似于:“负载通过 L1 缓存从全局可见存储获取其值,或通过本地存储进行转发。MFENCE 通过等待本地存储变为 GV 来消除第二种情况,在读取 L1 之前。” - BeeOnRope
1
抱歉,它是第3卷的第11.10节。我在该主题上看到类似的线程。我还没有其他线程的评论特权 :). 如果我开始另一个线程,它将是这些的副本。 - joz
显示剩余9条评论
3个回答

13

全局可见性对于负载来说是棘手的概念,因为负载不会修改内存的全局状态,并且其他线程无法直接观察到它。

但是一旦乱序/推测执行后尘埃落定,如果线程将其存储在某个地方或者根据其进行分支,则可以确定负载获取了什么值。线程的这种可观察行为是重要的。(或者我们可以通过调试器观察它,并/或者只是推断出负载可能看到的值,如果一个实验很困难的话)。


至少在像x86这样的强序CPU上,所有CPU都可以就存储器全局可见性达成一致的总顺序,从而更新单个一致性+一致性缓存+内存状态。在x86上,StoreStore reordering不允许,因此TSO(Total Store Order)与每个线程的程序顺序相一致。(即,总顺序是每个线程的程序顺序的某种交错)。SPARC TSO也是这种强序的。
(正确观察自己的存储相对于其他存储的全局顺序需要使用mfence或类似机制:否则,存储转发意味着您可以立即看到自己的存储,而它们还没有对其他核心可见。x86 TSO基本上是程序顺序加上存储转发。)
(对于绕过缓存的存储,全局可见性是指它们从私有写组合缓冲区刷新到DRAM时的情况。Intel Line Fill Buffers或任何等效的私有写组合机制,其中存储数据仍未对其他CPU可见,实际上是我们重新排序目的中存储缓冲区的一部分。)
在弱序ISA上,线程A和B可能无法就线程C和D执行的存储X和Y的顺序达成一致,即使读取线程使用acquire-loads确保自己的加载未被重新排序。即可能根本没有存储器的全局顺序,更不用说它与程序顺序不同了。
IBM POWER ISA很弱,C++11内存模型也是如此(不同线程中对不同位置的两次原子写入会被其他线程以相同顺序看到吗?)。但在POWER上实际使用的机制是(retired aka graduated)存储器在提交到L1d缓存之前,会在某些其他核心上变为可见状态,而不是全局可见。与所有正常CPU一样,缓存本身确实是协同的,并允许通过障碍来恢复顺序一致性。这些多重顺序效应只发生在SMT(一个物理CPU上的多个逻辑CPU)提供了一种查看来自其他逻辑核心的存储器的方法,而无需通过缓存。
(一个可能的机制是让其他逻辑线程从存储器缓冲区中窥视非规范性存储器,甚至在它们提交到L1d之前就进行这样做,只保留未退役的存储器私有于逻辑线程。这可以稍微减少线程间延迟。x86无法执行此操作,因为它会破坏强内存模型;当一个核心上有两个线程活动时,英特尔的HT会静态分割存储器缓冲区。但正如@BeeOnRope所评论的那样,一个关于允许重新排序的抽象模型可能是一种更好的推理正确性的方法。仅因为您无法想到一种硬件机制来引起重新排序并不意味着它不会发生。)
弱排序的指令集架构(ISA)如果没有使用屏障或释放型存储器,则仍然会在每个核心的本地存储缓冲区中进行重新排序,尽管实践上和/或在理论上不像POWER那么弱。在许多CPU上,所有存储都有全局顺序,但它并不是程序顺序的交错。乱序执行的CPU必须追踪内存顺序,以便单个线程无需屏障即可按顺序查看自己的存储,但允许存储从存储缓冲区提交到L1d时不按程序顺序进行排序,可能会显著提高吞吐量(特别是如果有多个存储器挂起等待同一行,但程序顺序会在每个存储器之间从关联式缓存中逐出该行,例如,恶劣的直方图访问模式)。

让我们进行一次有关负载数据来源的思想实验

以上仅涉及存储可见性,而非负载。 我们能否解释每个负载看到的值是在某个时刻从全局内存/缓存中读取的(不考虑任何负载顺序规则)?

如果可以,那么所有负载结果都可以通过将所有线程的所有存储和负载放入某个组合顺序来解释,读取和写入一个一致的全局内存状态。

事实证明,不行,存储缓冲区会破坏这一点:部分存储器到负载器的转发为我们提供了一个反例(例如,在x86上)。 窄存储器后跟宽负载器可以将存储器缓冲区的数据与在存储器变为全局可见之前从L1d缓存中获取的数据合并。 真正的x86 CPU实际上就是这样做的,我们有真实的实验来证明它。

如果仅考虑完整的存储器转发,即负载仅从存储器缓冲区中的一个存储器中获取其数据,则可以认为负载被存储器缓冲区延迟了。 也就是说,负载出现在使该值全局可见的存储器之后的全局总负载存储顺序中。

(这个全局总负载存储顺序并不是创建替代内存排序模型的尝试;它没有办法描述x86的实际负载排序规则。)


部分存储转发揭示了加载数据并非总是来自全局一致性缓存域的事实。

如果来自另一个核心的存储更改了周围的字节,原子宽度加载可能会读取一个在全局一致状态中从未存在过且永远不会存在的值。

请参阅我的答案Can x86 reorder a narrow store with a wider load that fully contains it?以及Alex的答案,证明了这种重新排序是可以发生的,使得该问题中提出的锁定方案无效。 从同一地址进行存储然后重新加载不是StoreLoad内存屏障

有些人(例如Linus Torvalds)将其描述为存储缓冲区不一致。(Linus回复的是另一个独立发明了相同无效锁定思想的人。)

涉及存储缓冲区和一致性的另一个Q&A:How to set bits of a bit vector efficiently in parallel?。您可以执行一些非原子OR以设置位,然后返回并检查由于与其他线程的冲突而导致的遗漏更新。但是,您需要一个StoreLoad屏障(例如x86 lock or),以确保重新加载时不仅看到自己的存储。


提议的定义:当一个负载读取其数据时,它变得全局可见。通常来自L1d,但存储缓冲区、MMIO或不可缓存内存是其他可能的来源。

该定义与x86手册一致,手册指出加载不会与其他加载重排序。即它们按程序顺序从本地核的内存视图中加载。

负载本身可以独立于任何其他线程是否可以从该地址加载该值而变得全局可见。

尽管也许根本不讨论可缓存加载的“全局可见性”更有意义,因为它们正在从某个地方拉数据,没有任何可见效果。只有不可缓存的负载(例如从MMIO区域)应被认为是可见的副作用。

(在x86上,不可缓存的存储和加载非常强制排序,因此我认为无法将存储转发到不可缓存存储。除非也许通过与UC加载访问相同物理页面的WB映射执行了存储。)


1
抽象地来讲,也许这样想会更容易理解:存在一个全局的总存储序列。例如,一个不做任何存储操作的代理始终遵守此顺序。现在,每个线程还有一组按顺序加载的程序序列。每个加载操作,按顺序,要么(a)以一致的方式从总存储序列中接收其值,要么(b)从本地存储中接收其值。所谓“一致的方式”,就是如果一个加载操作从T100(任意标签,数字越高表示后执行)接收其值,则下一个加载操作将从T100或之后接收它们的值。 - BeeOnRope
2
因此,解决方案是采用更窄的规则,明确地将来自同一CPU的存储器区分对待:从全局顺序或本地存储缓冲区(转发)中按顺序获取它们的值。这种情况会导致加载似乎与使用GO的周围加载无序执行,但这仅限于存储转发情况。实际上,许多并发算法不受存储转发的影响,因此将其效果缩小至关重要。顺便说一下,我真的推荐阅读x86-TSO论文。它详细介绍了这个问题。 - BeeOnRope
1
这里的SSO是我编造的一个术语:“单存储订单”,用来指代我认为你所询问的内容:SSO平台具有单个全局存储订单,并且从该订单读取的CPU都同意该顺序 - 但该顺序不一定与每个线程上存储的程序顺序一致(例如,本地存储缓冲区可能不按顺序提交)。现在的问题是,哪种检验方法可以揭示差异?IRIW似乎不是一个好的选择,除非查看详细的屏障语义。答案似乎是,实际上POWER和理论上的ARM都不是SSO。 - BeeOnRope
1
我对ARM和POWER的了解大部分来自于这篇论文,我强烈推荐。它详细介绍了两个平台,并混合关注ISA保证(即抽象)行为和硬件描述,实际上可能导致这些重新排序和非常有用的litmus测试。过去,我有点反对将硬件级别的重新排序讨论(例如,存储缓冲区、转发、乱序执行)与抽象ISA保证的硬件内存模型混合在一起,因为我认为如果重要部分... - BeeOnRope
@DanielNitzan:正如另一条评论中讨论的,应该理解该语句意味着任何读者都要控制本地重排序,如果他们想观察自己的存储在全局顺序中。我不知道是否要在我曾经说过的每个答案中进行编辑,特别是当这个答案已经详细介绍了存储转发的影响时。你是否认为我的回答具有误导性,或者你是否担心其他未来的读者? - Peter Cordes
显示剩余60条评论

2
让我稍微扩展一下问题并讨论实现存储-加载转发的正确性方面。(我认为Peter答案的后半部分直接回答了这个问题。)
存储-加载转发改变了加载的延迟时间,而不是它的可见性。除非由于某些错误推测而被刷新,否则存储最终都将变为全局可见。如果没有存储-加载转发,则加载必须等待所有冲突的存储退役。然后加载可以正常获取数据。
(冲突存储的确切定义取决于ISA的内存排序模型。在x86中,假设WB内存类型允许存储-加载转发,任何按程序顺序较早且目标物理内存位置与加载重叠的存储都是冲突的存储。)
虽然如果系统中有来自其他代理的并发冲突存储,那么这可能会改变所加载的值,因为外部存储可能在本地存储之后但在本地加载之前生效。通常,存储缓冲区不处于相干域中,因此存储-加载转发可能会降低发生这种情况的概率。这取决于存储-加载转发实现的限制;通常不能保证将会对任何特定的加载和存储操作进行转发。
存储-加载转发也可能导致全局内存顺序,而如果没有它,则不可能出现这种情况。例如,在x86的强模型中,允许存储-加载重新排序,与存储-加载转发一起可以使系统中的每个代理以不同的顺序查看所有内存操作。
一般来说,考虑一个有两个代理的共享内存系统。让S1(A,B)是具有存储-加载转发的序列A和B的可能全局内存顺序集合,而S2(A,B)是不具有存储-加载转发的序列A和B的可能全局内存顺序集合。 S1(A,B)和S2(A,B)都是所有合法全局内存顺序S3(A,B)集合的子集。存储-加载转发可以使S1(A,B)不是S2(A,B)的子集。这意味着如果S2(A,B)= S3(A,B),则存储-加载转发将是非法优化。
存储-加载转发可能会改变每个全局内存顺序发生的概率,因为它减少了加载的延迟时间。

1
粗体部分似乎明显是错误的。一个允许由于存储转发而重新排序的进程在该系统上可能具有内存顺序,但在没有存储转发的更严格的系统中不可能。作为相当琐碎的例子,考虑两个具有完全一致缓存的系统,一个具有存储缓冲区和存储转发,另一个则没有。任何一个系统都不会重新排序负载或存储执行相对于彼此。第二个系统将表现为顺序一致,而第一个系统则不会,并且将具有更多可能的内存顺序。 - BeeOnRope
例如,在第一个系统中,Vol 3,Intel SDM中的8.2.3.4的“Dekker算法失败”检验在第一个系统上是可能的(因为它在x86上),但在第二个系统上不可能。在这个测试中,每个线程都写入一个不同的内存位置,然后从另一个线程写入的内存位置读取。在一个顺序一致的系统中,这种重新排序是不可能的。存储转发和存储缓冲区绝对会影响可能的重新排序,这就是为什么像x86这样的系统通常被描述为“具有存储缓冲区的总存储顺序(暗示着转发)”。 - BeeOnRope
@BeeOnRope 让我们把讨论集中在仅涉及存储-加载转发(SLF)的内容上。考虑两个系统,一个使用 SLF,另一个不使用。SLF 对 8.2.3.4 中的示例没有影响,因此我不明白你在说什么。请注意,问题仅涉及 SLF。 - Hadi Brais
@BeeOnRope 我觉得这个问题特别关注SLF对内存排序的影响。我没有仔细思考过与存储缓冲区比较时会发生什么。你可能是正确的。但我的回答有意不涉及这一点。“与纯粹来自存储缓冲区的重新排序相比,SLF会导致额外的重新排序。”怎么样?你能举个例子吗? - Hadi Brais
2
只需查看英特尔指南中的示例8.2.3.5x86-TSO中的示例n6。这两个示例都是由存储器到加载器转发引起的。这是一种重新排序,在大多数设计中不会发生,但如果没有SLF,则相同但没有SLF的设计将直接矛盾于加粗部分。抛开这些,也许您应该使您的加粗部分更加精确:如果您的意思是“存在一个没有SLF的理论处理器设计,它可以展示与具有SLF的设计上存在的相同的重新排序”,那么当然 - 任何事情都有可能! - BeeOnRope
显示剩余10条评论

2
一项负载从预约站(RS)分派,并通过地址生成单元(AGU)进入为相应的重排序缓冲区(ROB)条目分配的负载缓冲区条目。当分配负载缓冲区条目时,它会被着色为此时最新的存储缓冲区ID(SBID)。所谓着色是指将存储缓冲区中最近存储的条目号(也称ID)插入到负载缓冲区条目中。存储缓冲区包括存储地址缓冲区(SAB)和存储数据缓冲区(SDB);每个存储在两者中都有一个条目(因为每个存储通常是2个微操作合并而成),它们都具有相同的索引(条目号,即SBID)。
我认为一旦地址有效,条目中的有效位就会被设置,这意味着它们已准备好分派(并在数据最终写回ROB时清除)。
还有一种猜测性的内存消歧预测器,可能参与设置有效位,以指示它被预测为不会与其所标记的SBID和存储缓冲区(SAB中的存储地址和SDB中的数据)之间的任何存储器发生别名。如果被预测为会发生别名,或者实际上确实发生了别名(即在存储缓冲区中搜索地址并使用SAB中的位掩码来确定条目是否能够满足它(位掩码表示字节的特权级别,即监管员/非监管员),并使用操作码中隐含的大小来获取正在存储的地址范围。如果可以满足,则从SDB条目中读取数据),它将使用SDB中的数据进行猜测性的存储到加载转发,并将数据插入到加载缓冲区中,在LB(加载缓冲区)中完成加载,但不从LB中退役。存储到加载转发确保读取永远不会与同一位置的旧写入重新排序,因为读取将始终使用存储到加载转发。我认为,在LFENCE的SBID之前的所有存储地址都需要在对LFENCE之后的存储进行预测之前计算出来。
如果没有预测到别名,负载将被调度(并且与其他负载相比,负载始终按照严格顺序调度,除非该负载具有非暂态命中或针对USWC(Uncacheable Speculative Write Combining memory type)内存的情况(尽管与存储不同,此时它不知道是否为USWC)。负载同时进入dTLB(数据TLB)/ L1d(L1数据缓存)。

任何时候,当存储地址在SAB中完成,并且其SBID小于或等于问题负载的彩色SBID(考虑到环绕),它可以使内存消歧预测失效,并且流水线被清空,因为流水线现在要么使用存储之前存储的陈旧数据进行存储到负载转发,要么使用来自实际上没有依赖关系的存储的错误存储到负载转发数据。

当数据被加载到指定的物理目标寄存器中时,ROB 中的数据变得有效。当 ROB 中的数据有效且退役指针指向该条目时,加载不再是推测性的并获得了一个高级位。如果设置了一个指示 SAB 尾指针和带颜色的 SBID 之间所有存储地址已计算的位,则加载可以从 LB 中退役(被移除)。除非它是一个 senior load 指令,在这种情况下,它现在可以执行,因为它是 senior 并已经从 ROB 中退役。 LFENCE被派发到加载缓冲区,只有在所有先前的微操作已从ROB中退役并且所有先前的加载指令已从ROB+LB中退役时(根据其声称具有的指令流序列化属性,它可能会在其自己的周期内退役,而不是在ROB中在其之前的1或2个其他指令中)。当ROB告诉加载指令它们可以退役(不再是推测性的)并且获取的数据有效且加载不再是内存推测性时,加载指令就会退役。LFENCE在加载缓冲区和ROB的尾部派发(直到所有读取缓冲区在全局可见之前无法退役。我认为这意味着它确保任何高级加载指令(在从ROB退役并被标记为高级之后执行的指令,例如 PREFETCH )都分配了读取缓冲区。常规加载分配读取缓冲区并读取它们的数据,在可以退役之前,它们在加载缓冲区中变为有效。在这种情况下,“全局可见”意味着所有先前的读取LFB(Line Fill Buffers)已经从环中接收到了线的全局通知(可能在包含数据的读取响应中之前到来,也可能打包到读取响应中,这可能意味着它必须等待所有读取完成而不是被确认)。当然,已经从MOB(Memory Order Buffer)退役的指令已经是全局可见的,因为它们的数据已经返回,但是高级加载指令可能尚未分配读取缓冲区或已经被确认为全局可见。这类似于全局可见存储的定义,在响应RFO(Read For Ownership)时,LFB的全局观察很可能是核心拥有该行的许可(独占访问),其他核心已被无效化,这将在实际写入该行的数据之前返回给核心,假设这将始终在响应失去该行权限的窥视之前写回)。当LFENCE派发时,L1d缓存将其视为nop,它完成,从LB中删除并变为senior,而在加载缓冲区中它之前被阻止派发到L1d缓存的微操作现在允许派发。

负载的全局可见性确实会影响其他核心的缓存一致性状态,这就是为什么我认为LFENCE要求负载具有全局可见性的原因。核心中的负载未命中会进入LLC(最后一级缓存),LLC的嗅探过滤器显示只有另一个核心拥有该行数据。如果有1个或更多核心拥有该行数据,则需要将其降级到S状态并导致其写回修改的数据。写入LLC的数据可以带有S状态和全局可见通知返回到请求核心。如果核心中的负载未命中而LLC也未命中,则LLC可能会立即发送全局可见通知,并将请求发送给主代理从内存中获取数据(或者在多插槽系统中,LLC必须等待主代理确认不需要嗅探其他核心,然后才能向核心发送全局可见通知)。

我认为高级负载是指不再是推测性的负载,正在等待数据返回并变为有效数据,或者已经有效,因此可以立即退役,而高级负载指令是指从ROB中退役后分派的指令。


2
@Noah 我需要添加参考文献。目前我已经让答案更易读了。这个模型几乎完成了,但仍有未知数,仍在不断完善中,是许多专利和头脑风暴的结合 - 唯一剩下的就是微基准测试或向英特尔的人询问。我应该澄清什么是确定的,什么部分是猜测的。很多细节直接来自P6专利,但我基本上是推断并写在桑迪桥客户端架构的背景下。 - Lewis Kelsey
2
uops可能不会按程序顺序从RS中分派,因为RS调度器使用pLRU,但LB条目是由分配器按顺序分配的,因此处理负载缓冲区的逻辑将强制执行正确的负载缓冲区条目进入dTLB/L1d的顺序。我说RS的好处,但这不是好处,这只是为什么拥有较小的RS而不是1:1扩展到ROB会付出代价的原因之一。 - Lewis Kelsey
1
@Noah 不,他的意思是负载执行顺序混乱(在AGU中),但由于MOB(SAB / SDB / LB)中相应条目已被分配(并填充了ROB条目编号),这些条目是按程序顺序分配器分配的,因此MOB知道负载的真实顺序,当它们实际发送到内存层次结构时,因为它们在连续的LB条目中。可能LB条目标记有目标PRF号码而不是ROB条目(与没有dest寄存器的fence或store不同),但是然后您需要将ROB no.标记为PRF regs,似乎是浪费。 - Lewis Kelsey
2
@Noah - 是的,这就是我想说的。负载可以乱序执行,包括内存路径。也就是说,在AB情况下,B加载可以在A开始之前从内存中获取其值(“完成”)。只要B线在L1D中保持,直到A的加载完成,这种重新排序就不可观测。即B先执行但我们可以保证其值在A完成之前没有更改,所以没有办法判断。这种重新排序对于像x86这样高性能的强有序CPU至关重要。 - BeeOnRope
2
“存储缓冲区搜索”在实践中的方式也很有趣:逐个搜索匹配地址将会太慢,因此实际上有一些类似CAM的结构,并且为了使其更便宜,只使用地址的子集,因此可能会出现错误命中和各种其他问题。请查看“推测性存储旁路”论文,其中详细描述了存储缓冲区的搜索方式以及欺骗它的方法。 - BeeOnRope
显示剩余57条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接