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

How to use software prefetch systematically?

在阅读 When should we use prefetch? and examples from Prefetching Examples? 中已接受的答案后,我在理解何时实际使用预取方面仍然存在很多问题。虽然这些答案提供了预取有用的示例,但它们没有解释如何在实际程序中发现它。好像是乱猜。

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

for (int i = 0; i < n; i++) {
   // some code
   double x = a[i];
   // some code
}

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

How to use software prefetch systematically?

快速回答是:不要

正如您正确分析的那样,预取是一种棘手的高级优化技术,不可移植且很少有用。

您可以使用分析来确定哪些代码部分形成了瓶颈,并使用 valgrind 等专用工具来尝试识别可以使用编译器内置函数避免的缓存未命中。

不要对此抱有太大期望,但要对代码进行概要分析,将优化工作集中在有用的地方。

另请记住,对于大型数据集,更好的算法可以击败效率较低的优化实现。

How can I see that software prefetch can help for my specific program?

您可以查看缓存未命中的比例。由于 hardware performance counters. You can get the list with perf list for example. The list is dependent of the target processor architecture but there are some generic events. For example, L1-dcache-load-misses, LLC-load-misses and LLC-store-misses. Having the amount of cache misses is not very useful unless you also get the number of load/store. There are generic counters like L1-dcache-loads, LLC-loads or LLC-stores. AFAIK, for the L2, there is no generic counters (at least on Intel processors) and you need to use specific hardware counters (for example l2_rqsts.miss on Intel Skylake-like processors). To get the overall statistics, you can use perf stat -e an_hardware_counter,another_one your_program. A good documentation can be found here.

perf 或 VTune 可用于获取此信息

当miss比例比较大的时候,你应该尝试优化代码,但这只是一个提示。事实上,对于您的应用程序,您可以在应用程序的关键 part/time 中有很多缓存命中但很多缓存未命中。因此,缓存未命中可能会在所有其他情况中丢失。与 SIMD 代码相比,标量代码中大量的 L1 缓存引用尤其如此。一种解决方案是仅分析应用程序的特定部分并使用它的知识以便朝着正确的方向进行调查。性能计数器实际上并不是一个自动搜索程序中问题的工具,而是一个帮助您validating/disproving一些假设提供一些提示[的工具。 =76=] 关于正在发生的事情。它为您提供解决神秘案件的证据,但所有工作都取决于您,侦探。

How I can locate the loads that suffer from cache misses the most?

有些硬件性能计数器是“精确”,意思是可以定位到产生事件的指令。这 非常有用 因为您可以分辨出哪些指令导致了最多的缓存未命中(尽管在实践中并不总是准确的)。您可以使用 perf record + perf report so 来获取信息(更多信息请参阅之前的教程)。

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

How to see the cache level where misses happen to decide which prefetch(0,1,2) to use?

这在实践中通常很难选择,并且非常依赖于您的应用程序。理论上,这个数字是一个 提示 来告诉处理器目标缓存行的位置级别(例如,提取到 L1、L2 或 L3 缓存中)。例如,如果您知道数据应该很快被读取和重用,那么将它放在 L1 中是个好主意。但是,如果使用了 L1 并且您不想用仅使用一次(或很少使用)的数据污染它,则最好将数据提取到较低的缓存中。在实践中,它有点复杂,因为从一种体系结构到另一种体系结构的行为可能不同......有关更多信息,请参阅

的用法示例。软件预取用于避免某些特定步骤的缓存垃圾问题。这是硬件预取器不是很有用的病态情况。

Assuming I found a particular load that suffers from the miss in a specific cache level, where should I place prefetch?

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

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

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
}

其中 magic_distance_guess 是通常基于基准设置的值(或对目标平台的非常深刻的理解,尽管实践经常表明甚至 highly-skilled 开发人员也无法找到最佳值)。

问题是延迟非常依赖于数据来自哪里目标平台在大多数情况下,除非开发人员在唯一的给定目标平台上工作,否则他们无法真正确切地知道何时进行预取。这使得软件预取难以使用,并且在目标平台发生变化时通常是有害的(必须考虑代码的可维护性和指令的开销)。更不用说 built-ins 是 compiler-dependent,预取内在函数是 architecture-dependent 并且 没有使用软件预取的标准可移植方式

Do I need to worry about unrolling the loop to make sure that I am prefetching only on cache line boundaries or it will be almost free like a nop if data is already in cache?

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

Is it worth to use multiple __builtin_prefetch calls in a row to prefetch multiple cache lines at once?

这非常依赖于目标平台。现代主流 x86-64 处理器以 out-of-order 的方式并行执行指令,并且它们分析了相当多的 window 指令。他们倾向于尽快执行加载以避免遗漏,他们通常非常适合此类工作。

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


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