在阅读了When should we use prefetch?中被接受的答案和Prefetching Examples?中的示例之后,我仍然对理解何时实际使用预取有很多问题。虽然这些答案提供了一个预取有用的例子,但它们并没有解释如何在真实的程序中发现它。看起来像是乱猜。
特别是,我对通过GCC的__builtin_prefetch
内部访问的Intel x86(prefetchnta,prefetcht2,prefetcht1,prefetcht0,prefetchw)的C实现感兴趣。我想知道:
- 我如何才能看到软件预取可以帮助我的特定程序?我想我可以收集CPU分析指标(例如高速缓存未命中数)。在这种情况下,什么指标(或它们之间的关系)表明有机会提高软件预取的性能?
- 如何定位最容易发生缓存未命中的加载?
- 如何查看发生未命中的该高速缓存级别以决定使用哪个预取(0,1,2)?
- 假设我发现了一个特定的加载,它在特定的缓存级别中遭受了未命中,我应该把预取放在哪里?作为一个例子,假设下一个循环遭受缓存未命中
for (int i = 0; i < n; i++) {
// some code
double x = a[i];
// some code
}
我应该在加载a[i]
之前还是之后放置预取?它应该指向a[i+m]
前方多远?我是否需要担心展开循环以确保我只在缓存行边界上预取,或者如果数据已经在缓存中,它将像nop
一样几乎是空闲的?在一行中使用多个__builtin_prefetch
调用来同时预取多个缓存行是否值得?
4条答案
按热度按时间hgqdbh6s1#
我如何才能看到软件预取可以帮助我的特定程序?
您可以检查缓存未命中的比例。
perf
或VTune可以通过hardware performance counters获取此信息。例如,您可以使用perf list
获取列表。该列表依赖于目标处理器体系结构,但存在一些通用事件。例如,L1-dcache-load-misses
、LLC-load-misses
和LLC-store-misses
。获得缓存未命中的数量不是很有用,除非您还获得加载/存储的数量。有通用计数器,如L1-dcache-loads
,LLC-loads
或LLC-stores
。AFAIK,对于L2,没有通用计数器(至少在Intel处理器上),您需要使用特定的硬件计数器(例如,在Intel Skylake类处理器上的l2_rqsts.miss
)。要获取总体统计信息,可以使用perf stat -e an_hardware_counter,another_one your_program
。一个好的文档可以在here中找到。当失败的比例很大时,你应该尝试优化代码,但这只是一个提示。事实上,对于应用程序,在应用程序的关键部分/时间,可能会有很多缓存命中,但也有很多缓存未命中。因此,高速缓存未命中可能在所有其它高速缓存未命中中丢失。这对于与SIMD的标量代码相比大量标量代码的L1高速缓存引用尤其如此。一个解决方案是只分析应用程序的特定部分,并使用它的知识,以便在好的方向上进行调查。性能计数器实际上并不是一个自动搜索程序中问题的工具,而是 * 一个帮助您验证/反驳某些假设 * 或 * 给予给予有关正在发生的事情的一些提示 * 的工具。它给你证据来解决一个神秘的案件,但它是由你,侦探,做所有的工作。
如何定位最容易发生缓存未命中的加载?
某些硬件性能计数器是“precise”,这意味着可以找到生成事件的指令。这是非常有用的,因为你可以知道哪些指令导致了最多的缓存未命中(尽管在实践中并不总是精确的)。您可以使用
perf record
+perf report
来获取信息(有关详细信息,请参阅上一个教程)。请注意有很多原因会导致缓存未命中,只有少数情况可以通过使用软件预取来解决。
如何查看发生未命中的该高速缓存级别以决定使用哪个预取(0,1,2)?
这在实践中通常很难选择,并且非常依赖于您的应用程序。从理论上讲,这个数字是一个提示,告诉处理器目标缓存行的局部性级别(例如,取到L1、L2或L3高速缓存中)。例如,如果您知道数据应该很快被读取和重用,那么将其放在L1中是一个好主意。但是,如果使用L1,并且您不想用只使用一次(或很少使用)的数据污染它,那么最好将数据提取到较低的缓存中。在实践中,它有点复杂,因为不同架构的行为可能不同……请参阅What are
_mm_prefetch()
locality hints?了解更多信息。用法的一个例子是这个问题。使用软件预取来避免具有一些特定步幅的高速缓存垃圾问题。这是一种病态的情况,其中硬件预取器不是非常有用。
假设我发现了一个特定的加载,它在特定的缓存级别中遭受了未命中,我应该把预取放在哪里?
这显然是最棘手的部分。您应该足够早地预取该高速缓存行,以便显著减少延迟,否则指令是无用的并且实际上可能是有害的。实际上,指令占用程序中的一些空间,需要被解码,并且使用可以用于执行例如其他(更关键的)加载指令的加载端口。但是,如果太晚了,那么该高速缓存行可能会被驱逐,需要重新加载.
通常的解决方案是编写这样的代码:
其中
magic_distance_guess
是一个通常基于基准测试(或对目标平台的深入理解,尽管实践经常表明即使是高技能的开发人员也无法找到最佳值)的值。问题是延迟非常依赖于 * 数据来自何处 * 和 * 目标平台 *。在大多数情况下,开发人员无法确切地知道何时进行预取,除非他们在特定的目标平台上工作。这使得软件预取使用起来很棘手,并且在目标平台改变时经常是有害的(必须考虑代码的可维护性和指令的开销)。更不用说内置程序依赖于编译器,预取内部函数依赖于体系结构,并且没有标准的可移植方式来使用软件预取。
我是否需要担心展开循环以确保我只在缓存行边界上预取,或者如果数据已经在缓存中,它将像nop一样几乎是空闲的?
是的,预取指令不是空闲的,因此最好每个高速缓存行仅使用1个指令(因为同一高速缓存行上的其他预取指令将是无用的)。
是否值得连续使用多个__builtin_prefetch调用来同时预取多个缓存行?
这非常依赖于目标平台。现代主流x86-64处理器以无序方式并行执行指令,并且它们具有相当大的指令分析窗口。他们倾向于尽快执行加载,以避免错过,他们通常非常适合这样的工作。
在你的例子循环中,我希望硬件预取器应该做得很好,而在(相对较新的)主流处理器上使用软件预取应该较慢。
十年前,当硬件预取器不是很智能时,软件预取是有用的,但现在它们往往非常好。另外,引导硬件预取器通常比使用软件预取指令更好,因为前者具有较低的开销。这就是为什么不鼓励软件预取(例如。英特尔和大多数开发人员),除非你真的知道你在做什么**。
dwthyt8l2#
快速的答案是:别这样
正如您正确分析的那样,预取是一种棘手且高级的优化技术,它不可移植,而且很少有用。
您可以使用分析来确定哪些代码段形成瓶颈,并使用专门的工具(如valgrind)来尝试和识别可能使用编译器内置避免的缓存未命中。
不要对此期望太高,但一定要对代码进行分析,以便将优化工作集中在有用的地方。
还要记住,对于大型数据集,一个更好的算法可以击败效率较低的算法的优化实现。
p1iqtdky3#
如其他答案所提到的,SW预取器的管理需要大量的手动工作,并且难以跨不同的系统和工作负载进行概括。现代CPU上的HW预取器已经取得了足够的进展,并且可以识别不同的内存访问模式。
虽然这篇文章有点旧,但[1]从2012年开始广泛讨论了HW预取器和SW预取器,包括您的问题。作者认为SW预取器适用于短数组、连续读取和不规则读取等场景。
有趣的是,在现代系统中仍然有许多模式不能被硬件预取器很好地识别,例如 * 点追逐 *。如果是multi-get或任务中存在一定的计算延迟,则可以使用SW预取来隐藏内存访问延迟。例如,[2,3]提出了一种相对通用的设计解决方案,该解决方案使用协程来将计算与SW预取重叠以隐藏数据读取延迟。
特别是当访问不快或带宽不足的存储器(如DRAM)时,SW预取器的优点将进一步增加。此外,HW预取器甚至可能通过不正确的预取数据影响除高速缓存之外的组件的性能。
qmelpv7a4#
GCC有一个
-fprefetch-loop-arrays
选项,但我不建议将其用于一般用途,仅作为对特定循环进行微基准测试时的实验。(https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html#index-fprefetch-loop-arrays)一些
-mtune=whatever
选项可以实现这一点,对于已知HW预取器可以使用帮助的CPU(并且前端带宽足够高,可以处理运行预取指令的额外吞吐量成本,而不会通常减慢速度,特别是如果数据在L2或L1 d缓存中已经很热。有一些
--param
调优参数,如--param prefetch-minimum-stride=n
,可以限制它仅在指针增量为多个缓存行或其他内容时生成预取指令。(现代x86 CPU中的硬件预取器可以处理跨步访问模式,尽管硬件预取器通常不跨4K边界工作,因为连续的虚拟页面可能不会Map到连续的虚拟页面。乱序的exec可以将请求加载生成到下一页,这通常就足够了。)参见“每个程序员都应该知道的关于内存的知识”中有多少仍然有效?- SW预取通常不值得。