根据我的测试,所有类型的预取指令都会消耗最新 Intel 主流 CPU 上的行填充缓冲区。
特别是I added some load & prefetch tests to 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-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
fb_full 值大于周期数,这意味着 LFB 几乎一直都是满的(它可能大于周期数,因为每个周期最多两个负载可能需要一个 LFB)。这个工作负载是纯粹的预取,所以除了预取之外没有什么可以填充 LFB。
此测试的结果也符合 Leeor 引用的手册部分中声称的行为:
在某些情况下,PREFETCH 不会执行数据预取。
其中包括:
- ...
- 如果内存子系统用完请求缓冲区
一级缓存和二级缓存之间。
显然这里不是这种情况:当 LFB 填满时,预取请求不会被丢弃,而是像正常加载一样停止,直到资源可用(这不是不合理的行为:如果您要求软件预取,你可能想得到它,即使这意味着拖延)。
我们还注意到以下有趣的行为:
-
prefetcht1 和 prefetcht2 之间似乎有一些小的差异,因为它们报告了 16-KiB 测试的不同性能(差异有所不同,但始终不同),但如果您重复测试,您将看到这更有可能只是运行间的变化,因为这些特定值有些不稳定(大多数其他值都非常稳定)。
- 对于 L2 包含的测试,我们每个周期可以承受 1 个负载,但只能承受一个
prefetcht0 预取。这有点奇怪,因为prefetcht0 应该与负载非常相似(在 L1 情况下,它每个周期可以发出 2 个)。
- 即使 L2 有约 12 个周期延迟,我们也能够完全隐藏延迟 LFB,仅使用 10 个 LFB:我们获得每个负载 1.0 个周期(受 L2 吞吐量限制),而不是
12 / 10 == 1.2 周期每个负载,我们' d 期望(最佳情况)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 limits L3 cache pollution (to something like only one way per set),所以我们似乎被驱逐了。
会不会不一样?
这是我在测试之前写的一个较旧的答案,推测它是如何工作的:
一般来说,我希望任何导致数据在 L1 中结束的预取 都会消耗行填充缓冲区,因为我相信 L1 和内存层次结构的其余部分之间的唯一路径是LFB1。因此,针对 L1 的 SW 和 HW 预取可能都使用 LFB。
但是,这使得以 L2 或更高级别为目标的预取不消耗 LFB 的可能性仍然存在。对于硬件预取,我很确定是这种情况:您可以找到许多参考资料来解释硬件预取是一种机制,可以有效地获得超出 LFB 提供的最大 10 的内存并行度。此外,L2 预取器似乎无法根据需要使用 LFB:它们居住在 L2 中/附近并向更高级别发出请求,可能使用超级队列并且不需要 LFB。
剩下的是针对 L2(或更高版本)的软件预取,例如 prefetcht1 和 prefetcht22。与 L2 生成的请求不同,这些请求从核心开始,因此它们需要某种方式从核心发出,这可能是通过 LFB。来自英特尔优化指南有以下有趣的引述(强调我的):
一般来说,软件预取到 L2 会显示出更多的好处
比 L1 预取。软件预取到 L1 将消耗关键
硬件资源(填充缓冲区),直到缓存线填充完成。 一个
软件预取到 L2 不持有这些资源,它是
不太可能对绩效产生负面影响。如果你确实使用 L1
软件预取,最好是软件预取服务
通过在 L2 缓存中的命中,所以硬件的时间长度
资源被最小化。
这似乎表明软件预取不消耗 LFB - 但此引用仅适用于 Knights Landing 架构,我无法为任何更主流的架构找到类似的语言。看来Knights Landing的缓存设计明显不同(或者引用错误)。
1 事实上,我认为即使是非临时存储也使用 LFB 来脱离执行核心——但它们的占用时间很短,因为它们一到达 L2可以进入超级队列(实际上不进入 L2)然后释放它们关联的 LFB。
2 我认为这两个都针对最近英特尔的 L2,但这也不清楚 - 也许t2 提示实际上针对某些 uarch 上的 LLC?