在阅读了When should we use prefetch?篇公认的答案和Prefetching Examples?篇的例子之后,我仍然有很多问题需要理解何时真正使用预回迁.虽然这些答案提供了一个预取有用的例子,但它们并没有解释如何在实际程序中发现预取.这看起来像是随机猜测.

特别是,我对英特尔x86的C实现(prefetchnta、prefetcht2、prefetcht1、prefetcht0、prefetchw)感兴趣,这些实现可以通过GCC的__builtin_prefetch个内部版本访问.我想知道:

  • 我怎样才能看到软件预取对我的特定程序有帮助?我想我可以用英特尔Vtune或Linux utility perf收集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调用一次预取多条缓存线是否值得?

推荐答案

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

您可以判断缓存未命中的比例.由于使用了hardware performance counters,因此可以使用perf或VTune来获取此信息.例如,你可以得到perf list的列表.该列表取决于目标处理器体系 struct ,但也有一些通用事件.例如,L1-dcache-load-missesLLC-load-missesLLC-store-misses.拥有缓存未命中的数量不是很有用,除非您还获得了加载/存储的数量.有L1-dcache-loadsLLC-loadsLLC-stores等通用计数器.另外,对于L2,没有通用计数器(至少在英特尔处理器上),您需要使用特定的硬件计数器(例如,在类似英特尔Skylake的处理器上使用l2_rqsts.miss).要获得总体统计数据,可以使用perf stat -e an_hardware_counter,another_one your_program.好的文档可以在here中找到.

当未命中的比例很大时,您应该try 优化代码,但这只是一个提示.事实上,关于应用程序,在应用程序的关键部分/时间,您可能会有很多缓存命中,但很多缓存未命中.因此,缓存未命中可能会丢失.与SIMD相比,标量代码中大量的一级缓存引用尤其如此.一种解决方案是只分析应用程序的特定部分,并利用它的知识朝着好的方向进行调查.性能计数器并不是一个真正的工具来自动搜索程序中的问题,而是a tool to assist you in validating/disproving some hypothesisgive some hints个关于正在发生的事情.它为你提供了破案的证据,但所有的工作都取决于你这个侦探.

如何找到缓存丢失最严重的负载?

一些硬件性能计数器为"precise",这意味着可以找到生成事件的指令.这是very useful,因为您可以判断哪些指令导致了最多的缓存未命中(尽管在实践中并不总是精确).您可以使用perf record+perf report来获取信息(有关更多信息,请参阅上一篇教程).

注意there are many reasons that can cause a cache misses and only few cases can be solved by using software prefetching.

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

这通常很难在实践中 Select ,并且非常依赖于您的应用程序.理论上,这个数字是hint,用来告诉处理器目标缓存线的局部性级别(例如,取入一级、二级或三级缓存).例如,如果您知道数据应该很快被读取和重用,那么最好将其放在L1中.但是,如果使用了L1,并且您不想用只使用一次(或很少使用)的数据污染L1,那么最好将数据提取到较低的缓存中.在实践中,这有点复杂,因为从一个架构到另一个架构的行为可能不一样...更多信息请参见What are _mm_prefetch() locality hints?.

例如使用this question.软件预取是用来避免缓存 destruct 问题的,有一些具体的步骤.这是一种病态的情况,硬件预取器不是很有用.

假设我在特定的缓存级别发现了一个因未命中而受到影响的特定负载,那么我应该将预取放在哪里?

这显然是最棘手的部分.您应该足够早地预取缓存线,以便显著减少延迟,否则该指令将无用且无效.实际上,该指令占用了程序中的一些空间,需要进行解码,并使用加载端口来执行其他(更关键的)加载指令.但是,如果太晚了,缓存线可能会被逐出,需要重新加载...

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

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是一个通常基于基准设定的值(或对目标平台的深刻理解,尽管实践经常表明,即使是高技能的开发人员也无法找到最佳值).

问题是延迟非常依赖于where data are coming fromtarget platform.In most case, developers cannot really know exactly when to do the prefetching unless they work on a unique given target platform.这使得软件预取很难使用,而且在目标平台发生变化时(必须考虑代码的可维护性和指令的开销)往往是有害的.更不用说内置函数依赖于编译器,预取内部函数依赖于体系 struct ,还有no standard portable way to use software prefetching个.

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

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

一行使用多个_内置_预取调用一次预取多个缓存线是否值得?

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

在您的示例循环中,我希望在(相对较新的)主流处理器上有the hardware prefetcher should do a very good job and using software prefetching should be slower个.


十年前,当硬件预取器不是很智能时,软件预取很有用,但现在它们往往非常好.此外,指导硬件预取程序通常比使用软件预取指令更好,因为前者的开销更低.这就是为什么software prefetching is discouraged (eg. by Intel and most developers) unless you really know what you are doing.

C++相关问答推荐

Ebpf内核代码:permission denied:invalid access to map value

C/SDL程序,渲染不使用我的渲染器

Can函数指针指向C++中具有不同参数连续性的函数

在C23中使用_GENERIC实现带有右值的IS_POINTER(P)?

使用双指针动态分配和初始化2D数组

是什么让numpy.sum比优化的(自动矢量化的)C循环更快?

为什么将函数名括在括号中会禁用隐式声明?

防止C++中递归函数使用堆栈内存

等同于铁 rust 的纯C语言S未实现!()宏

在C中访问数组中的特定值

OpenSSL:如何将吊销列表与SSL_CTX_LOAD_VERIFY_LOCATIONS一起使用?

C:在编译时构建和使用字符串文字的预处理器宏?

处理EPOLL_WAIT中的接收数据和连接关闭信号

C语言中神秘的(我认为)缓冲区溢出

C中的char**v*char[]

Matlab/Octave对conv2函数使用哪种方法?

在C中打印指针本身

为什么argc和argv即使在主函数之外也能工作?

c如何传递对 struct 数组的引用,而不是设置 struct 的副本

段错误try 访问静态字符串,但仅有时取决于构建环境