基于我的测试,最近的英特尔主流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测试,
prefetcht1
和prefetcht2
之间似乎存在一些小差异(差异大小不同,但始终存在),但如果您重复测试,您会发现这更可能只是运行到运行的变化,因为那些特定值有点不稳定(大多数其他值非常稳定)。
- 对于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的限制。这里
prefetcht1
和prefetcht2
始终比加载或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(或更高级别)的软件预取,例如prefetcht1
和prefetcht2
2呢?与由L2生成的请求不同,这些请求始于核心,因此它们需要一种从核心到外部的方式,这可能是通过LFB。从英特尔优化指南中有以下有趣的引用(重点在我):
一般而言,将数据预取到L2缓存中会比预取到L1缓存中表现更好。将数据预取到L1缓存中会占用关键的硬件资源(填充缓冲区),直到缓存行完全填充为止。将数据预取到L2缓存中则不会占用这些资源,并且不太可能对性能产生负面影响。如果确实要使用L1数据预取,请尽量使其在L2缓存中命中,以最小化占用硬件资源的时间。这似乎表明软件预取不会占用LFBs,但是这段引文仅适用于Knights Landing架构,并且我无法在任何其他主流架构中找到类似的语言。看起来,Knights Landing的缓存设计与其他架构显著不同(或者这段引文是错误的)。
1 实际上,我认为即使是非时间存储器也使用LFBs来脱离执行核心 - 但它们的占用时间很短,因为一旦它们到达L2,它们就可以进入超级队列(实际上不进入L2),然后释放其关联的LFB。
2 我认为在英特尔的最新产品中,这两者都针对L2,但这也不是很清楚 - 或许t2
提示实际上在某些uarchs上针对LLC?