如何系统地使用软件预取?

Cur*_*ous 5 c performance x86 profiling prefetch

阅读何时应该使用预取?中接受的答案后 以及预取示例中的示例,我在理解何时实际使用预取方面仍然存在很多问题。虽然这些答案提供了预取很有用的示例,但它们没有解释如何在实际程序中发现它。看起来像是随机猜测。

我特别对 intel x86 的 C 实现(prefetchnta、prefetcht2、prefetcht1、prefetcht0、prefetchw)感兴趣,这些实现可以通过 GCC 的__builtin_prefetch内在函数访问。我想知道:

  • 我如何才能看到软件预取对我的特定程序有帮助?我想我可以使用 Intel Vtune 或 Linux 实用程序收集 CPU 分析指标(例如缓存未命中次数)perf。在这种情况下,什么指标(或它们之间的关系)表明有机会通过软件预取来提高性能?
  • 如何找到缓存未命中最严重的负载?
  • 如何查看发生未命中的缓存级别来决定使用哪个预取(0,1,2)?
  • 假设我发现一个特定的负载在特定的缓存级别中遭受缺失,我应该在哪里放置预取?例如,假设下一个循环出现缓存未命中
for (int i = 0; i < n; i++) {
   // some code
   double x = a[i];
   // some code
}
Run Code Online (Sandbox Code Playgroud)

我应该在加载之前还是之后放置预取a[i]?它应该指向前方多远a[i+m]?我是否需要担心展开循环以确保我仅在缓存行边界上预取,或者它几乎是免费的,就像nop数据已经在缓存中一样?是否值得__builtin_prefetch连续使用多个调用来一次预取多个缓存行?

Jér*_*ard 4

我如何才能看到软件预取对我的特定程序有帮助?

您可以检查缓存未命中的比例。perf借助硬件性能计数器,可以使用 VTune 来获取此信息。perf list例如,您可以获取该列表。该列表取决于目标处理器架构,但有一些通用事件。例如,L1-dcache-load-missesLLC-load-missesLLC-store-misses。除非您还获得了加载/存储的数量,否则获得缓存未命中的数量并不是很有用。有一些通用计数器,例如L1-dcache-loads,LLC-loadsLLC-stores。AFAIK,对于 L2,没有通用计数器(至少在英特尔处理器上),您需要使用特定的硬件计数器(例如l2_rqsts.miss在类似英特尔 Skylake 的处理器上)。要获取总体统计数据,您可以使用perf stat -e an_hardware_counter,another_one your_program. 可以在这里找到很好的文档。

当未命中的比例很大时,那么你应该尝试优化代码,但这只是一个提示。事实上,对于您的应用程序,您的应用程序的关键部分/时间可能会出现大量缓存命中,但也会出现许多缓存未命中。因此,缓存未命中可能会在所有其他缓存缺失中丢失。对于与 SIMD 相比具有大量标量代码的 L1 缓存引用来说尤其如此。一种解决方案是仅分析应用程序的特定部分,并利用它的知识来朝好的方向进行调查。性能计数器并不是真正自动搜索程序中问题的工具,而是帮助您验证/反驳某些假设或给出有关正在发生的情况的一些提示的工具。它为你提供解决神秘案件的证据,但所有工作都取决于你,侦探,来完成所有的工作。

如何找到缓存未命中最严重的负载?

一些硬件性能计数器是“精确的”,这意味着可以定位生成事件的指令。这非常有用,因为您可以判断哪些指令造成了最多的缓存未命中(尽管在实践中并不总是准确的)。您可以使用perf record+ perf reportso 来获取信息(有关更多信息,请参阅前面的教程)。

请注意,导致缓存未命中的原因有很多,并且只有少数情况可以通过使用软件预取来解决

如何查看发生未命中的缓存级别来决定使用哪个预取(0,1,2)?

这在实践中通常很难选择,并且很大程度上取决于您的应用程序。理论上,该数字是一个提示,告诉处理器目标缓存行的局部性级别(例如,提取到 L1、L2 或 L3 缓存中)。例如,如果您知道数据应该很快被读取和重用,那么将其放在 L1 中是一个好主意。但是,如果使用 L1 并且您不希望只使用一次(或很少使用)的数据污染它,那么最好将数据提取到较低的缓存中。实际上,它有点复杂,因为一种架构的行为可能与另一种架构不同......请参阅什么是_mm_prefetch()局部性提示?了解更多信息。

这个问题就是一个用法示例。使用软件预取来避免某些特定步长的缓存垃圾问题。这是一种病态的情况,硬件预取器不是很有用。

假设我发现某个特定负载在特定缓存级别中遭受缺失,我应该在哪里放置预取?

这显然是最棘手的部分。您应该尽早预取高速缓存行,以便显着减少延迟,否则该指令是无用的,实际上可能是有害的。事实上,该指令在程序中占用了一些空间,需要进行解码,并使用可用于执行其他(更关键的)加载指令的加载端口。但是,如果为时已晚,则缓存行可能会被逐出并需要重新加载......

通常的解决方案是编写如下代码:

for (int i = 0; i < n; i++) {
   // some code
   const size_t magic_distance_guess = 200;
   __builtin_prefetch(&data[i+magic_distance_guess]);
   double x = a[i];
   // some code
}
Run Code Online (Sandbox Code Playgroud)

通常根据基准设置的值在哪里magic_distance_guess(或者对目标平台的非常深入的了解,尽管实践经常表明即使是高技能的开发人员也无法找到最佳值)。

问题是延迟很大程度上取决于数据的来源目标平台在大多数情况下,开发人员无法真正知道何时进行预取,除非他们在唯一的给定目标平台上工作。这使得软件预取使用起来很棘手,并且当目标平台发生变化时通常是有害的(必须考虑代码的可维护性和指令的开销)。更不用说内置函数是依赖于编译器的,预取内在函数是依赖于体系结构的,并且没有标准的可移植方法来使用软件预取

我是否需要担心展开循环以确保仅在缓存行边界上预取,或者如果数据已在缓存中,则它几乎像 nop 一样免费?

是的,预取指令不是免费的,因此最好每个缓存行仅使用 1 个指令(因为同一缓存行上的其他预取指令将毫无用处)。

是否值得连续使用多个 __builtin_prefetch 调用来一次预取多个缓存行?

这非常依赖于目标平台。现代主流 x86-64 处理器以无序方式并行执行指令,并且它们分析的指令窗口相当大。他们倾向于尽快执行负载以避免失误,并且他们通常非常适合此类工作。

在您的示例循环中,我希望硬件预取器应该做得很好,并且在(相对较新的)主流处理器上使用软件预取应该会更慢。


十年前,当硬件预取器还不是很智能时,软件预取非常有用,但现在它们往往非常好。此外,引导硬件预取器通常比使用软件预取指令更好,因为前者的开销较低。这就是为什么不鼓励软件预取(例如英特尔和大多数开发人员),除非您真的知道自己在做什么