24

我已经意识到,利特尔定律限制了在给定延迟和给定并发级别下传输数据的速度。如果您想更快地传输某些东西,您要么需要更大的传输,要么需要更多的“飞行中”传输,要么需要更低的延迟。对于从 RAM 读取的情况,并发性受 Line Fill Buffer 数量的限制。

当加载未命中 L1 高速缓存时,将分配行填充缓冲区。现代英特尔芯片(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 个行填充缓冲区之一的情况下从内存中的新位置启动获取,从而通过绕过利特尔定律来实现更高的带宽?

4

2 回答 2

12

根据我的测试,所有类型的预取指令都会消耗最新 Intel 主流 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,我们可能会期望它比加载变体更快。

更确切地说,我们可以检查l1d_pend_miss.fb_full计数器,其描述为:

请求需要 FB(填充缓冲区)条目但没有可用条目的次数。请求包括加载、存储或软件预取指令的可缓存/不可缓存需求。

描述已经表明 SW 预取需要 LFB 条目,并且测试证实了这一点:对于所有类型的预取,对于并发是限制因素的任何测试,这个数字都非常高。例如,对于 512-KiBprefetcht1测试:

 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                  

fb_full值大于周期数,这意味着 LFB 几乎一直都是满的(它可能超过周期数,因为每个周期最多两个负载可能需要一个 LFB)。这个工作负载是纯粹的预取,所以除了预取之外没有什么可以填满 LFB。

该测试的结果还收缩了 Leeor 引用的手册部分中声称的行为:

在某些情况下,PREFETCH 不会执行数据预取。这些包括:

  • ...
  • 如果内存子系统耗尽了一级缓存和二级缓存之间的请求缓冲区。

显然这里不是这种情况:当 LFB 填满时,预取请求不会被丢弃,而是像正常加载一样停止,直到资源可用(这不是不合理的行为:如果您要求软件预取,您可能想要得到它,也许即使这意味着拖延)。

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

  • 似乎和之间有一些小的差异prefetcht1prefetcht2因为他们报告了 16-KiB 测试的不同性能(差异有所不同,但始终不同),但如果您重复测试,您会发现这更有可能只是运行 -运行变化,因为这些特定值有些不稳定(大多数其他值非常稳定)。
  • 对于 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始终比负载快 0.3 个周期或prefetcht0. 给定 10 个 LFB,这等于减少了 3 个周期的占用,这或多或少可以解释为预取在 L2 处停止,而不是一直到 L1。
  • prefetchtnta通常具有比 L1 之外的其他吞吐量低得多的吞吐量。这可能意味着它prefetchtnta实际上正在做它应该做的事情,并且似乎将线带入 L1,而不是 L2,并且只是“弱”进入 L3。因此,对于包含 L2 的测试,它具有并发限制的吞吐量,就好像它正在命中 L3 缓存一样,而对于 2048-KiB 的情况(L3 缓存大小的 1/3),它具有命中主内存的性能。 prefetchnta限制 L3 缓存污染(每组只有一种方式),所以我们似乎被驱逐了。

会不会不一样?

这是我在测试之前写的一个较旧的答案,推测它是如何工作的:

一般来说,我希望任何导致数据最终进入 L1的预取都会消耗行填充缓冲区,因为我相信 L1 和内存层次结构的其余部分之间的唯一路径是 LFB 1。因此,针对 L1 的 SW 和 HW 预取可能使用 LFB。

但是,这留下了以 L2 或更高级别为目标的预取不消耗 LFB 的可能性。对于硬件预取,我很确定是这种情况:您可以找到许多参考资料来解释硬件预取是一种有效地获得超出 LFB 提供的最大值 10 的内存并行度的机制。此外,L2 预取器似乎无法根据需要使用 LFB:它​​们居住在 L2 中/附近并向更高级别发出请求,可能使用超级队列并且不需要 LFB。

这留下了针对 L2(或更高)的软件预取,例如prefetcht1and prefetcht22。与 L2 生成的请求不同,这些请求从核心开始,因此它们需要某种方式从核心发出,这可能是通过 LFB。来自英特尔优化指南有以下有趣的引述(强调我的):

通常,软件预取到 L2 将显示出比 L1 预取更多的好处。软件预取到 L1 将消耗关键的硬件资源(填充缓冲区),直到缓存线填充完成。预取到 L2 的软件不会保留这些资源,因此不太可能对性能产生负面影响。如果您确实使用 L1 软件预取,则最好通过 L2 缓存中的命中来服务软件预取,这样可以最大限度地减少保留硬件资源的时间长度。

这似乎表明软件预取不消耗 LFB - 但此引用仅适用于 Knights Landing 架构,我无法为任何更主流的架构找到类似的语言。看来 Knights Landing 的缓存设计明显不同(或引用错误)。


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

2我认为这两个都针对最近英特尔的 L2,但这也不清楚 - 也许t2暗示实际上针对某些 uarch 上的 LLC?

于 2017-12-24T08:21:15.297 回答
10

首先是一个小的修正——阅读优化指南,你会注意到一些硬件预取器属于 L2 缓存,因此不受填充缓冲区数量的限制,而是受 L2 对应项的限制。

“空间预取器”(您指的 colocated-64B 行,完成到 128B 块)就是其中之一,因此理论上,如果您获取每隔一行,您将能够获得更高的带宽(一些 DCU 预取器可能会尝试“为您填补空白”,但理论上它们应该具有较低的优先级,因此它可能会起作用)。

然而,“王”预取器是另一个家伙,“L2 流媒体”。第 2.1.5.4 节内容如下:

Streamer :此预取器监视来自 L1 缓存的读取请求,以获取地址的升序和降序序列。受监控的读取请求包括由加载和存储操作以及硬件预取器发起的 L1 DCache 请求,以及用于代码提取的 L1 ICache 请求。当检测到请求的前向或后向流时,会预取预期的缓存行。预取的缓存行必须在同一个 4K 页面中

重要的部分是——

流媒体可以在每次 L2 查找时发出两个预取请求。流媒体可以在加载请求之前运行多达 20 行

这个 2:1 的比例意味着对于这个预取器识别的访问流,它总是在你的访问之前运行。确实,您不会在 L1 中自动看到这些行,但这确实意味着如果一切正常,您应该始终为它们获得 L2 命中延迟(一旦预取流有足够的时间提前运行并减轻 L3/内存延迟)。您可能只有 10 个 LFB,但正如您在计算中指出的那样 - 访问延迟越短,您更换它们的速度越快,您可以达到的带宽就越高。这实质上是将延迟分离为和的L1 <-- mem并行流。L1 <-- L2L2 <-- mem

至于标题中的问题 - 尝试填充 L1 的预取将需要一个行填充缓冲区来保存该级别的检索数据。这可能应该包括所有 L1 预取。至于 SW 预取,第 7.4.3 节说:

在某些情况下,PREFETCH 不会执行数据预取。这些包括:

  • PREFETCH 导致 DTLB(数据转换后备缓冲区)未命中。这适用于具有对应于系列 15、型号 0、1 或 2 的 CPUID 签名的 Pentium 4 处理器。PREFETCH 解决 DTLB 未命中并在具有对应于系列 15、型号 3 的 CPUID 签名的 Pentium 4 处理器上获取数据。
  • 对导致故障/异常的指定地址的访问。
  • 如果内存子系统耗尽了一级缓存和二级缓存之间的请求缓冲区。

...

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

于 2013-10-20T18:05:59.597 回答