根据我的测试,所有类型的预取指令都会消耗最新 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 填满时,预取请求不会被丢弃,而是像正常加载一样停止,直到资源可用(这不是不合理的行为:如果您要求软件预取,您可能想要得到它,也许即使这意味着拖延)。
我们还注意到以下有趣的行为:
- 似乎和之间有一些小的差异
prefetcht1
,prefetcht2
因为他们报告了 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 的限制。这里
prefetcht1
和prefetcht2
始终比负载快 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(或更高)的软件预取,例如prefetcht1
and prefetcht2
2。与 L2 生成的请求不同,这些请求从核心开始,因此它们需要某种方式从核心发出,这可能是通过 LFB。来自英特尔优化指南有以下有趣的引述(强调我的):
通常,软件预取到 L2 将显示出比 L1 预取更多的好处。软件预取到 L1 将消耗关键的硬件资源(填充缓冲区),直到缓存线填充完成。预取到 L2 的软件不会保留这些资源,因此不太可能对性能产生负面影响。如果您确实使用 L1 软件预取,则最好通过 L2 缓存中的命中来服务软件预取,这样可以最大限度地减少保留硬件资源的时间长度。
这似乎表明软件预取不消耗 LFB - 但此引用仅适用于 Knights Landing 架构,我无法为任何更主流的架构找到类似的语言。看来 Knights Landing 的缓存设计明显不同(或引用错误)。
1事实上,我认为即使是非临时存储也使用 LFB 来脱离执行核心——但是它们的占用时间很短,因为它们一旦到达 L2 就可以进入超级队列(实际上不需要进入 L2 ) 然后释放它们关联的 LFB。
2我认为这两个都针对最近英特尔的 L2,但这也不清楚 - 也许t2
暗示实际上针对某些 uarch 上的 LLC?