软件预取是否分配线填充缓冲区(LFB)?

28

我意识到,在给定延迟和并发水平的情况下,Little's Law 限制了数据传输速度。如果想要更快地传输数据,则需要更大的传输量、更多的“飞行中”传输或更低的延迟。对于从 RAM 读取的情况,同时可以处理的数量受 Line Fill Buffers 的数量限制。

当加载未命中 L1 缓存时,会分配一个 Line Fill Buffer。现代 Intel 芯片(Nehalem、Sandy Bridge、Ivy Bridge、Haswell)每个核心有 10 个 LFB,因此每个核心最多只能同时处理 10 个缓存未命中。如果 RAM 延迟为 70 ns(合理),每次传输为 128 字节(64B 缓存线及其硬件预取的孪生数据),则每个核心的带宽限制为:10 * 128B / 75 ns = ~16 GB/s。单线程的Stream等基准测试证实了这一点相当准确。

降低延迟的明显方法是使用 x64 指令 PREFETCHT0、PREFETCHT1、PREFETCHT2 或 PREFETCHNTA 预取所需数据,以避免从 RAM 读取。但我使用它们并未能加快任何操作。问题似乎在于 __mm_prefetch() 指令本身会消耗 LFB,因此它们也受到相同的限制。硬件预取不会触及 LFB,但也无法跨页边界。

但我无法在任何地方找到这方面的文档资料。 我找到的最接近的是15年前的一篇文章,提到Pentium III上的预取使用了Line Fill Buffers。 我担心自那时以来情况可能已经发生了变化。由于我认为LFB与L1缓存相关,我不确定为什么向L2或L3进行预取会消耗它们。 然而,我测量得到的速度表明这是可能的。

所以:有没有办法在不使用这10个Line Fill Buffers的情况下从新的内存位置开始获取数据,从而通过绕过Little's Law实现更高的带宽?


非常有趣的阅读。你有进一步解决这个问题吗?以下是我的一些观察。在Sandy Bridge笔记本电脑上,每个核心可以达到16GB/s(峰值为21GB/s)。此外,我已经测试了一个Nehalem E7-4870,每个插槽的峰值带宽为34GB/s。在单个核心上,我只能实现5GB/s,尽管这两种架构都有10个填充缓冲区。使用您的模型,这将表明200ns的延迟要大3倍,我认为这不是现实的。这两种架构的延迟应该是可比较的。 - angainor
@angainor:许多核心的Xeon在uncore方面具有更高的延迟,因此比笔记本/台式机芯片具有更低的单核心 L3/内存带宽(有关详细信息和链接,请参见此答案的“延迟限制平台”部分)。同时,多插槽也会带来一定影响:必须在从本地DRAM加载之前窥视另一个插槽的缓存。Xeon的优势在于从许多线程中获得的聚合内存带宽。 - Peter Cordes
我认为Skylake已经将LFB的数量从10个增加到了12个。 - BeeOnRope
2个回答

15
基于我的测试,最近的英特尔主流CPU上,所有类型的预取指令都会消耗线填充缓冲区。
特别地,在我向uarch-bench添加了一些负载和预取测试,它们使用各种大小的缓冲区进行大步幅负载。以下是我在Skylake i7-6700HQ上获得的典型结果:
                     Benchmark   Cycles    Nanos
  16-KiB parallel        loads     0.50     0.19
  16-KiB parallel   prefetcht0     0.50     0.19
  16-KiB parallel   prefetcht1     1.15     0.44
  16-KiB parallel   prefetcht2     1.24     0.48
  16-KiB parallel prefetchtnta     0.50     0.19

  32-KiB parallel        loads     0.50     0.19
  32-KiB parallel   prefetcht0     0.50     0.19
  32-KiB parallel   prefetcht1     1.28     0.49
  32-KiB parallel   prefetcht2     1.28     0.49
  32-KiB parallel prefetchtnta     0.50     0.19

 128-KiB parallel        loads     1.00     0.39
 128-KiB parallel   prefetcht0     2.00     0.77
 128-KiB parallel   prefetcht1     1.31     0.50
 128-KiB parallel   prefetcht2     1.31     0.50
 128-KiB parallel prefetchtnta     4.10     1.58

 256-KiB parallel        loads     1.00     0.39
 256-KiB parallel   prefetcht0     2.00     0.77
 256-KiB parallel   prefetcht1     1.31     0.50
 256-KiB parallel   prefetcht2     1.31     0.50
 256-KiB parallel prefetchtnta     4.10     1.58

 512-KiB parallel        loads     4.09     1.58
 512-KiB parallel   prefetcht0     4.12     1.59
 512-KiB parallel   prefetcht1     3.80     1.46
 512-KiB parallel   prefetcht2     3.80     1.46
 512-KiB parallel prefetchtnta     4.10     1.58

2048-KiB parallel        loads     4.09     1.58
2048-KiB parallel   prefetcht0     4.12     1.59
2048-KiB parallel   prefetcht1     3.80     1.46
2048-KiB parallel   prefetcht2     3.80     1.46
2048-KiB parallel prefetchtnta    16.54     6.38

需要注意的关键点是,所有预取技术在任何缓冲区大小下都没有比加载更快。如果任何预取指令不使用LFB,我们期望它对于适合它预取到的缓存级别的基准测试非常快。例如,prefetcht1将行带入L2,因此对于128-KiB测试,如果它不使用LFB,则可能比load变体更快。
更具决定性的是,我们可以检查l1d_pend_miss.fb_full计数器,其描述为:
“请求需要FB(Fill Buffer)条目但没有可用条目的次数。请求包括可缓存/不可缓存的负载、存储或SW预取指令。”
描述已经表明SW预取需要LFB条目,并且测试已经证实:对于所有类型的预取,在并发是一个限制因素的任何测试中,此数字都非常高。例如,对于512-KiB的prefetcht1测试:
 Performance counter stats for './uarch-bench --test-name 512-KiB parallel   prefetcht1':

        38,345,242      branches                                                    
     1,074,657,384      cycles                                                      
       284,646,019      mem_inst_retired.all_loads                                   
     1,677,347,358      l1d_pend_miss.fb_full                  
< p > fb_full的值大于循环次数,这意味着LFB几乎一直都是满的(由于每个周期最多可有两个负载想要一个LFB,因此可能超过循环次数)。此工作负载纯预取,因此除预取之外没有任何内容可以填充LFB。

此测试的结果也与Leeor引用的手册部分中声称的行为相矛盾:

有些情况下,PREFETCH将不执行数据预取。 这些包括:

  • ...
  • 如果第一级缓存和第二级缓存之间的请求缓冲区耗尽。

显然,这里并非如此:当LFB填满时,预取请求不会被丢弃,而是像正常负载一样被暂停,直到资源可用(这不是不合理的行为:如果您要求软件预取,您可能希望得到它,即使这意味着暂停)。

我们还注意到以下有趣的行为:

  • 对于16-KiB测试,prefetcht1prefetcht2之间似乎存在一些小差异(差异大小不同,但始终存在),但如果您重复测试,您会发现这更可能只是运行到运行的变化,因为那些特定值有点不稳定(大多数其他值非常稳定)。
  • 对于L2包含的测试,我们可以每个周期维持1次加载,但只能进行一次prefetcht0预取。这有点奇怪,因为prefetcht0应该与加载非常相似(在L1情况下它可以每个周期发出2次)。
  • 尽管L2具有约12个周期的延迟,但我们能够仅使用10个LFB完全隐藏LFB的延迟:我们每次加载获得1.0个周期(受L2吞吐量限制),而不是我们期望的每次加载12/10 == 1.2个周期(最佳情况),如果LFB是限制因素(非常低的fb_full值证实了这一点)。这可能是因为12个周期的延迟是从加载到执行核心的所有完整延迟,其中还包括几个周期的额外延迟(例如,L1延迟为4-5个周期),因此实际在LFB中花费的时间少于10个周期。
  • 对于L3测试,我们看到3.8-4.1个周期的值,非常接近基于L3加载到使用延迟的期望值42/10 = 4.2个周期。因此,当我们达到L3时,我们肯定受到10个LFB的限制。这里prefetcht1prefetcht2始终比加载或prefetcht0快0.3个周期。鉴于10个LFB,这相当于占用减少了3个周期左右,可以更多或更少地解释为预取停止在L2而不是一直到L1。
  • prefetchtnta通常比L1以外的其他预取具有更低的吞吐量。这可能意味着prefetchtnta实际上正在执行其预期的操作,并且似乎将行带入L1而不是L2,只是“弱化”地带入L3。因此,对于L2包含的测试,它具有并发限制的吞吐量,就像它命中L3缓存一样,并且对于2048-KiB情况(L3缓存大小的1/3),它具有命中主存储器的性能。prefetchnta限制L3缓存污染(例如每个集合只有一条路),因此我们似乎正在获得驱逐。

可能会有所不同吗?

以下是我在测试之前写的旧答案,对它如何工作进行了推测:

一般而言,我期望任何导致数据进入L1缓存的预取都会消耗一行填充缓冲区,因为我认为L1和内存层次结构之间唯一的路径就是LFB1。因此,针对L1的软件和硬件预取可能都使用LFB。

然而,这也意味着针对L2或更高级别的预取可能不会消耗LFB。对于硬件预取的情况,我非常确定这是正确的:您可以找到许多参考资料,解释HW预取是一种机制,可有效地获得超过LFB提供的最大10个内存并行性。此外,如果L2预取器想要使用LFB,似乎它们无法使用:它们位于/接近L2,并向更高级别发出请求,可能使用超级队列,并且不需要LFB。

那么,针对L2(或更高级别)的软件预取,例如prefetcht1prefetcht22呢?与由L2生成的请求不同,这些请求始于核心,因此它们需要一种从核心到外部的方式,这可能是通过LFB。从英特尔优化指南中有以下有趣的引用(重点在我):

一般而言,将数据预取到L2缓存中会比预取到L1缓存中表现更好。将数据预取到L1缓存中会占用关键的硬件资源(填充缓冲区),直到缓存行完全填充为止。将数据预取到L2缓存中则不会占用这些资源,并且不太可能对性能产生负面影响。如果确实要使用L1数据预取,请尽量使其在L2缓存中命中,以最小化占用硬件资源的时间。这似乎表明软件预取不会占用LFBs,但是这段引文仅适用于Knights Landing架构,并且我无法在任何其他主流架构中找到类似的语言。看起来,Knights Landing的缓存设计与其他架构显著不同(或者这段引文是错误的)。

1 实际上,我认为即使是非时间存储器也使用LFBs来脱离执行核心 - 但它们的占用时间很短,因为一旦它们到达L2,它们就可以进入超级队列(实际上不进入L2),然后释放其关联的LFB。

2 我认为在英特尔的最新产品中,这两者都针对L2,但这也不是很清楚 - 或许t2提示实际上在某些uarchs上针对LLC?


你认为NT存储器在前往LFB的路上是否使用了L1D?英特尔将它们描述为使用LFB作为写组合缓冲区,因此我认为这意味着存储直接进入LFB。我认为加载执行单元直接连接到LFB以及L1D以支持在数据到达以满足需求加载(也用于从USWC内存进行movntdqa加载)时的早期重启,因此直接连接到存储单元似乎也是有可能的。 - Peter Cordes
@PeterCordes - 顺便说一下,尽管最初的版本是一个纯粹的打字错误,但我认为NT存储器确实与L1交互,因为对于一个存储到相应行在L1中是脏的位置的存储器,需要从L1中填充任何未写入的字节,然后再刷新它。是的,关于NT存储器有更松散的排序保证,但并不是那么松散,以至于它们可以忽略L1中同一行中的数据(而且无论如何,它们都被记录为刷新这些行)。因此,可能在NT存储器的LFB分配时会探测L1,如果存在,则使用L1内容填充LFB,或者可能发生在刷新时。 - BeeOnRope
正确的,NT存储器与常规存储器混合仍然被当前线程严格按程序顺序观察到。我不知道将NT和非NT存储器混合到同一行中对性能有多大影响。我不知道是否可以将L1D行移动到LFB中;这样做不能将常规存储器转换为NT存储器;否则可能会导致它们被重新排序。我不会感到惊讶如果脏的L1D数据必须写回才能为NT存储器分配LFB。 - Peter Cordes
是的,我有另一个回复,但我没有发布,但它的意思是:即我的信念是,相对于常规存储器而言,NT存储器的非排序主要是关于它们在写组合缓冲区(LFB)中的位置,这可能不会被其他核心的探测所探测到,并且它们不会通过本地存储器执行的RFO进行排序(如果它们尚未在本地M)。一旦该行变为全局可见(例如,命中L3),我认为它以与其他非NT操作相同的方式协同。 - BeeOnRope
1
暂时忽略NT存储,我基本上认为整个L1 + LFB + L2可以视为单个单元(“私有缓存”)以实现并发。就外部请求行和必须响应来自其他核心的嗅探而言,它作为一个整体。如果一个核心具有M状态的行,并且来自另一个核心的嗅探进入,并且该行正在从L1到L2逐出中间阶段,会发生什么?一种选择是该行可能仅存在于LFB中,这可能意味着必须探测LFB... - BeeOnRope
显示剩余12条评论

12
首先进行一个小修正——阅读优化指南,您会注意到一些硬件预取器属于L2高速缓存,因此不受填充缓冲区数量限制,而是受L2副本的限制。
“空间预取器”(您所提到的协同64B行,补充到128B块之一)就是其中之一,因此理论上,如果您获取每隔一行,您将能够获得更高的带宽(一些DCU预取器可能会尝试“为您填补空白”,但理论上它们应该具有较低的优先级,因此可能有效)。
然而,“王牌”预取器是另一个家伙,即“L2流式处理器”。第2.1.5.4节如下所述:
该预取器监视L1高速缓存中地址的升序和降序读请求。被监视的读请求包括由加载和存储操作以及硬件预取器发起的L1 DCache请求,以及用于代码获取的L1 ICache请求。检测到前向或后向请求流时,将预取期望的高速缓存行。预取的高速缓存行必须在同一页的4K页面内。
重要部分是- 该流式传输器可以在每个L2查找上发出两个预取请求。该流式传输器可以提前20行加载请求。
这个2:1的比率意味着,对于被此预取器识别的访问流,它将始终超前于您的访问。确实,您不会自动在L1中看到这些行,但这意味着,如果一切正常,您应该始终获得它们的L2命中延迟(一旦预取流有足够的时间超前并缓解L3/内存延迟)。您可能只有10个LFB,但正如您在计算中指出的那样-访问延迟变短,您可以更快地替换它们,达到更高的带宽。这本质上是将延迟分离成L1 <-- L2L2 <-- mem的并行流。
至于您标题中的问题-合理的做法是,试图填充L1的预取需要一个行填充缓冲区来保存该级别检索到的数据。这可能应该包括所有L1预取。至于软件预取,第7.4.3节说:
有时PREFETCH无法执行数据预取。这些情况包括:
- PREFETCH导致DTLB(数据转换后备缓存)缺失。这适用于CPUID签名对应于家族15、型号0、1或2的Pentium 4处理器。PREFETCH解决了CPUID签名对应于家族15、型号3的Pentium 4处理器上的DTLB缺失并提取数据。 - 访问指定地址引起故障/异常。 - 如果一级缓存和二级缓存之间的请求缓冲区内存子系统已经用尽。

所以我认为你是对的,SW预取不是人为增加未完成请求数量的方法。然而,同样的解释也适用于这里——如果你知道如何使用SW预取提前访问你的行,你可能能够减轻一些访问延迟并增加有效带宽。但是,这对于长流不起作用有两个原因:1)你的缓存容量是有限的(即使预取是暂时的,比如t0 flavor),2)你仍然需要为每个预取支付完整的L1->mem延迟,因此你只是把压力向前移了一点——如果你的数据操作比内存访问快,你最终会赶上你的SW预取。因此,只有在你能够足够提前预取所有你需要的东西并将其保留在那里时,这才起作用。


感谢提供详细信息!我同意L2和L3硬件预取(HPF)不使用LFB,这就是我们从8 GB/s到16 GB/s的原因。我不知道L1 HPF是否使用LFB。是的,英特尔似乎记录了NTA和T0软件预取通过LFB加载L1。但是T1和T2呢?看起来它们不会,但基准测试并不一致。也许并发性还有其他限制因素?是的,目标是使用SWP增加带宽,但我没有成功地获得任何显着的好处。关键似乎是提示从Mem到L3/L2的负载,而不使用LFB。 - Nathan Kurz
关于L1硬件预取 - 我在同一份文档中看到,发出它们的条件之一是“没有太多其他加载未命中正在进行”。也许这表明它们也在竞争相同的LFB,因此这被用作压力减轻的手段。 - Leeor

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