我们什么时候应该使用预取?

大宝剑*_*大宝剑 6 performance x86 arm prefetch

某些CPU和编译器提供预取指令.例如:GCC文档中的 __builtin_prefetch .虽然GCC的文件中有评论,但它对我来说太短了.

我想知道,在prantice中,我们应该何时使用预取?有一些例子吗?谢谢!

Lee*_*eor 7

这个问题并不是关于编译器的,因为它们只是提供一些钩子来将预取指令插入到汇编代码/二进制文件中.不同的编译器可能提供不同的内在格式,但您可以忽略所有这些并且(小心地)将它直接添加到汇编代码中.

现在真正的问题似乎是"什么时候预取有用",答案是 - 在任何情况下你都会受到内存延迟的限制,并且访问模式不规则且可区分用于捕获的HW预取(在流中组织)或者大步),或者当您怀疑有太多不同的流供HW同时跟踪时.
大多数编译器只会很少为您插入自己的预取,因此基本上由您来使用您的代码并预测预取可能有用.

@Mysticial的链接显示了一个很好的例子,但是这是一个我认为不能被HW抓住的更直接的例子:

#include "stdio.h"
#include "sys/timeb.h"
#include "emmintrin.h"

#define N 4096
#define REP 200
#define ELEM int

int main() {
    int i,j, k, b;
    const int blksize = 64 / sizeof(ELEM);
    ELEM __attribute ((aligned(4096))) a[N][N];
    for (i = 0; i < N; ++i) {
        for (j = 0; j < N; ++j) {
            a[i][j] = 1;
        }
    }
    unsigned long long int sum = 0;
    struct timeb start, end;
    unsigned long long delta;

    ftime(&start);
    for (k = 0; k < REP; ++k) {
        for (i = 0; i < N; ++i) {
            for (j = 0; j < N; j ++) {
                sum += a[i][j];
            }
        }
    }
    ftime(&end);
    delta = (end.time * 1000 + end.millitm) - (start.time * 1000 + start.millitm);
    printf ("Prefetching off: N=%d, sum=%lld, time=%lld\n", N, sum, delta); 

    ftime(&start);
    sum = 0;
    for (k = 0; k < REP; ++k) {
        for (i = 0; i < N; ++i) {
            for (j = 0; j < N; j += blksize) {
                for (b = 0; b < blksize; ++b) {
                    sum += a[i][j+b];
                }
                _mm_prefetch(&a[i+1][j], _MM_HINT_T2);
            }
        }
    }
    ftime(&end);
    delta = (end.time * 1000 + end.millitm) - (start.time * 1000 + start.millitm);
    printf ("Prefetching on:  N=%d, sum=%lld, time=%lld\n", N, sum, delta); 
}
Run Code Online (Sandbox Code Playgroud)

我在这里做的是遍历每个矩阵行(享受连续行的HW预取器帮助),但是从位于不同页面的下一行(具有硬件预取应该被硬按压)中具有相同列索引的元素前面预取去抓).我总结数据只是为了没有优化它,重要的是我基本上只是循环一个矩阵,应该非常简单和易于检测,但仍然得到加速.

使用gcc 4.8.1 -O3构建,它使我在英特尔至强X5670上的性能提升了近20%:

Prefetching off: N=4096, sum=3355443200, time=1839
Prefetching on:  N=4096, sum=3355443200, time=1502
Run Code Online (Sandbox Code Playgroud)

请注意,即使我使控制流更复杂(额外的循环嵌套级别),也会收到加速,分支预测器应该很容易捕获该短块大小循环的模式,并且它可以节省不需要的预取的执行.

请注意,Ivybridge和onward on 应该有一个"下一页预取器",因此HW可能能够在这些CPU上缓解这一点(如果有人有一个可用并且想要尝试我会很高兴知道).在那种情况下,我会修改基准以对每一行进行求和(并且预取将每次向前看两行),这应该混淆硬件预取器的地狱.

Skylake的结果

以下是Skylake i7-6700-HQ的一些结果,运行频率为2.6 GHz(无涡轮增压)gcc:

编译标志: -O3 -march=native

Prefetching off: N=4096, sum=28147495993344000, time=896
Prefetching on:  N=4096, sum=28147495993344000, time=1222
Prefetching off: N=4096, sum=28147495993344000, time=886
Prefetching on:  N=4096, sum=28147495993344000, time=1291
Prefetching off: N=4096, sum=28147495993344000, time=890
Prefetching on:  N=4096, sum=28147495993344000, time=1234
Prefetching off: N=4096, sum=28147495993344000, time=848
Prefetching on:  N=4096, sum=28147495993344000, time=1220
Prefetching off: N=4096, sum=28147495993344000, time=852
Prefetching on:  N=4096, sum=28147495993344000, time=1253
Run Code Online (Sandbox Code Playgroud)

编译标志: -O2 -march=native

Prefetching off: N=4096, sum=28147495993344000, time=1955
Prefetching on:  N=4096, sum=28147495993344000, time=1813
Prefetching off: N=4096, sum=28147495993344000, time=1956
Prefetching on:  N=4096, sum=28147495993344000, time=1814
Prefetching off: N=4096, sum=28147495993344000, time=1955
Prefetching on:  N=4096, sum=28147495993344000, time=1811
Prefetching off: N=4096, sum=28147495993344000, time=1961
Prefetching on:  N=4096, sum=28147495993344000, time=1811
Prefetching off: N=4096, sum=28147495993344000, time=1965
Prefetching on:  N=4096, sum=28147495993344000, time=1814
Run Code Online (Sandbox Code Playgroud)

因此,使用预取要么约40%,要么快8%,具体取决于您是否使用-O3-O2分别用于此特定示例.最大的减速-O3实际上是由于代码生成的怪癖:在-O3没有预取的循环中进行了矢量化,但预取变量循环的额外复杂性无论如何都会阻止我的gcc版本上的矢量化.

所以-O2结果可能更多的是苹果对苹果,而且我们在Leeor's Westmere看到的好处大约是一半(8%加速比16%).仍然值得注意的是,你必须小心不要改变代码生成,这样你就会大幅放缓.

这个测试可能是不打算在这一理想int通过int引用大量的CPU开销,而不是强调内存子系统(这就是为什么矢量帮助这么多).


  • 谢谢.你对int开销是正确的,如果我们只查看每个缓存行中的一个元素,那么可以进一步扩展好处(这不是一个现实的场景,但是对于什么向量化可能给出的一个很好的代理 (2认同)

Bee*_*ope 6

在最近的英特尔芯片上,您显然可能想要使用预取的一个原因是避免CPU节能功能人为地限制您实现的内存带宽.在这种情况下,简单的预取可以将性能提高一倍,而不是预取的相同代码,但它完全取决于所选的电源管理计划.

我跑了一个简化版本(代码在这里的测试中)Leeor的回答,强调存储器子系统多一点(因为这就是预取会有所帮助,受伤或什么都不做).最初的测试强调CPU与内存子系统并行,因为它int在每个缓存行上都加在一起.由于典型的内存读取带宽在15 GB/s的范围内,即每秒37.5亿个整数,因此对最大速度设置了相当大的限制(未向量化的代码通常int每个周期处理1个或更少,因此3.75 GHz CPU将大致相同的CPU和内存数量).

首先,我得到的结果似乎在我的i7-6700HQ(Skylake)上显示了预先踢屁股:

Prefetching off: SIZE=256 MiB, sum=1407374589952000, time=221, MiB/s=11583
Prefetching  on: SIZE=256 MiB, sum=1407374589952000, time=153, MiB/s=16732
Prefetching off: SIZE=256 MiB, sum=1407374589952000, time=221, MiB/s=11583
Prefetching  on: SIZE=256 MiB, sum=1407374589952000, time=160, MiB/s=16000
Prefetching off: SIZE=256 MiB, sum=1407374589952000, time=204, MiB/s=12549
Prefetching  on: SIZE=256 MiB, sum=1407374589952000, time=160, MiB/s=16000
Prefetching off: SIZE=256 MiB, sum=1407374589952000, time=200, MiB/s=12800
Prefetching  on: SIZE=256 MiB, sum=1407374589952000, time=160, MiB/s=16000
Prefetching off: SIZE=256 MiB, sum=1407374589952000, time=201, MiB/s=12736
Prefetching  on: SIZE=256 MiB, sum=1407374589952000, time=157, MiB/s=16305
Prefetching off: SIZE=256 MiB, sum=1407374589952000, time=197, MiB/s=12994
Prefetching  on: SIZE=256 MiB, sum=1407374589952000, time=157, MiB/s=16305
Run Code Online (Sandbox Code Playgroud)

在预测数字的情况下,预取可以达到16 GiB/s以上,而不仅仅是12.5左右,因此预取可以将速度提高约30%.对?

没那么快.记住省电模式在现代芯片上有各种各样的精彩互动,我将我的Linux CPU 调控器从默认的powersave 1改为性能.现在我得到:

Prefetching off: SIZE=256 MiB, sum=1407374589952000, time=155, MiB/s=16516
Prefetching  on: SIZE=256 MiB, sum=1407374589952000, time=157, MiB/s=16305
Prefetching off: SIZE=256 MiB, sum=1407374589952000, time=153, MiB/s=16732
Prefetching  on: SIZE=256 MiB, sum=1407374589952000, time=144, MiB/s=17777
Prefetching off: SIZE=256 MiB, sum=1407374589952000, time=144, MiB/s=17777
Prefetching  on: SIZE=256 MiB, sum=1407374589952000, time=153, MiB/s=16732
Prefetching off: SIZE=256 MiB, sum=1407374589952000, time=152, MiB/s=16842
Prefetching  on: SIZE=256 MiB, sum=1407374589952000, time=153, MiB/s=16732
Prefetching off: SIZE=256 MiB, sum=1407374589952000, time=153, MiB/s=16732
Prefetching  on: SIZE=256 MiB, sum=1407374589952000, time=159, MiB/s=16100
Prefetching off: SIZE=256 MiB, sum=1407374589952000, time=163, MiB/s=15705
Prefetching  on: SIZE=256 MiB, sum=1407374589952000, time=161, MiB/s=15900
Run Code Online (Sandbox Code Playgroud)

这是一个完全的折腾.有和没有预取似乎表现相同.因此,在高功率节省模式下,硬件预取都不那么激进,或者存在一些与节能相关的其他交互,其与显式软件预取的行为不同.

调查

事实上,如果你改变benchark,预取与否之间的区别就更加极端了.现有基准在预取开启和关闭之间交替进行,并且事实证明这有助于"关闭"变体,因为在"开启"测试中发生的速度增加部分地延续到随后的关闭测试2.如果运行"关闭"测试,则会得到大约9 GiB/s的结果:

Prefetching off: SIZE=256 MiB, sum=1407374589952000, time=280, MiB/s=9142
Prefetching off: SIZE=256 MiB, sum=1407374589952000, time=277, MiB/s=9241
Prefetching off: SIZE=256 MiB, sum=1407374589952000, time=285, MiB/s=8982
Run Code Online (Sandbox Code Playgroud)

...相对于预取版本大约17 GiB/s:

Prefetching  on: SIZE=256 MiB, sum=1407374589952000, time=149, MiB/s=17181
Prefetching  on: SIZE=256 MiB, sum=1407374589952000, time=148, MiB/s=17297
Prefetching  on: SIZE=256 MiB, sum=1407374589952000, time=148, MiB/s=17297
Run Code Online (Sandbox Code Playgroud)

因此预取版本几乎快两倍.

让我们来看看perf stat**off*版本的内容:

'./prefetch-test off'的效果计数器统计信息:

   2907.485684      task-clock (msec)         #    1.000 CPUs utilized                                          
 3,197,503,204      cycles                    #    1.100 GHz                    
 2,158,244,139      instructions              #    0.67  insns per cycle        
   429,993,704      branches                  #  147.892 M/sec                  
        10,956      branch-misses             #    0.00% of all branches     
Run Code Online (Sandbox Code Playgroud)

......而版本:

   1502.321989      task-clock (msec)         #    1.000 CPUs utilized                          
 3,896,143,464      cycles                    #    2.593 GHz                    
 2,576,880,294      instructions              #    0.66  insns per cycle        
   429,853,720      branches                  #  286.126 M/sec                  
        11,444      branch-misses             #    0.00% of all branches
Run Code Online (Sandbox Code Playgroud)

不同之处在于预取的版本始终以~2.6 GHz的最大非turbo频率运行(我通过MSR禁用了turbo).然而,没有预取的版本决定以1.1 GHz的低得多的速度运行.如此大的CPU差异通常也反映了非核心频率的巨大差异,这可以解释更糟糕的带宽.

现在我们已经看过这个,它可能是最新英特尔芯片上的节能Turbo功能的结果,当他们确定一个进程主要是内存限制时,试图降低CPU频率,可能是因为CPU核心速度增加了在这些情况下提供很多好处.正如我们在这里看到的,这种假设并不总是正确的,但我不清楚这种权衡是否总体上是一个坏的,或者启发式只是偶尔会错误.


1我正在运行intel_pstate驱动程序,这是最新内核上的Intel芯片的默认设置,它实现了"硬件p状态",也称为"HWP".使用的命令:sudo cpupower -c 0,1,2,3 frequency-set -g performance.

2相反,"关闭"测试的减速部分延续到"开启"测试,尽管效果不那么极端,可能是因为节省的"加速"行为比"减速"行为更快.


Bee*_*ope 6

以下是我所知道的案例的简要总结,在这些案例中,软件预取可能特别有用。有些可能不适用于所有硬件。

应该从这样的角度来阅读这个列表:最明显可以使用软件预取的地方是可以在软件中预测访问流的地方,但是这种情况对于软件预取来说不一定是明显的胜利,因为out-无序处理通常最终会产生类似的效果,因为它可以在现有的未命中之后执行,以便在飞行中获得更多的未命中。

因此,这个列表更多的是“鉴于软件预取并不像乍看起来那么明显有用,这里有一些地方它可能仍然有用”,通常与直接释放的替代方案相比 -无序处理完成它的工作,或者只是使用“普通加载”在需要之前加载一些值。

在无序窗口中安装更多负载

尽管乱序处理可能会暴露与软件预取相同类型的 MLP(内存级并行),但缓存未命中后可能的总先行距离存在固有的限制。其中包括重新排序缓冲区容量、加载缓冲区容量、调度程序容量等。请参阅这篇博客文章,了解额外工作严重阻碍 MLP 的示例,因为 CPU 无法提前运行足够远的距离来立即执行足够的负载。

在这种情况下,软件预取允许您有效地在指令流中更早地填充更多负载。举个例子,假设您有一个循环,它执行一次加载,然后对加载的数据执行 20 条指令,并且您的 CPU 有一个包含 100 条指令的无序缓冲区,并且加载彼此独立(例如, .以已知步长访问数组)。

第一次未命中后,您可以再运行 99 条指令,其中包括 95 条非加载指令和 5 条加载指令(包括第一次加载)。因此,您的 MLP 本质上受到乱序缓冲区大小的限制,为 5。相反,如果您将每个加载与两个软件预取配对到一个位置,例如提前 6 个或更多迭代,那么您最终会得到 90 个非加载指令、5 个加载和 5 个软件预取,因为所有这些加载只是将您的 MLP 加倍至 10 2

当然,每次加载一次额外的预取没有限制:您可以添加更多预取以达到更高的数字,但是当您达到机器的 MLP 限制时,会有一个递减点,然后是负回报,并且预取会占用您的资源我宁愿把钱花在其他事情上。

这类似于软件流水线,您为未来的迭代加载数据,然后在完成大量其他工作之前不要触摸该寄存器。这主要用于有序机器上,以隐藏计算和内存的延迟。即使在具有 32 个架构寄存器的 RISC 上,软件流水线通常也无法像现代机器上的最佳预取距离那样将负载放置在使用之前。自早期的有序 RISC 以来,CPU 在一个内存延迟期间可以完成的工作量已经增加了很多。

订购机器

并非所有机器都是乱序核心:有序 CPU 在某些地方(尤其是 x86 之外)仍然很常见,而且您还会发现无法运行的“弱”乱序核心领先很远,因此在某种程度上就像有序的机器一样。

在这些机器上,软件预取可能有助于获得您无法访问的 MLP(当然,有序机器可能不支持大量固有的 MLP)。

解决硬件预取限制

硬件预取可能有限制,您可以使用软件预取来解决这些限制。

例如,Leeor 的答案有一个硬件预取在页面边界停止的示例,而软件预取没有任何此类限制。

另一个例子可能是硬件预取过于激进或过于保守的任何时候(毕竟它必须猜测您的意图):您可以使用软件预取,因为您确切地知道应用程序的行为方式。

后者的示例包括预取不连续区域:例如较大矩阵的子矩阵中的行:硬件预取不会理解“矩形”区域的边界,并且会不断预取超出每行末尾的内容,然后取需要一点时间来学习新的行模式。软件预取可以完全正确地实现这一点:根本不发出任何无用的预取(但它通常需要丑陋的循环分割)。

如果您进行了足够的软件预取,理论上硬件预取应该大部分关闭,因为内存子系统的活动是它们用来决定是否激活的一种启发式方法。

对位法

我应该在这里指出,当谈到硬件预取可以加快的情况下可能的加速时,软件预取并不等同于硬件预取:硬件预取可以快得多。这是因为硬件预取可以在更靠近内存的地方(例如,从 L2)开始工作,在那里它具有较低的内存延迟,并且还可以访问更多的缓冲区(在英特尔芯片上所谓的“超级队列”中),因此具有更高的并发性。因此,如果您关闭硬件预取并尝试memcpy使用纯软件预取来实现一个或某些其他流加载,您会发现它可能会更慢。

特殊负载提示

预取可以让您访问常规加载无法实现的特殊提示。例如,x86 具有prefetchntaprefetcht0prefetcht1prefetchw指令,它们向处理器提示如何处理缓存子系统中加载的数据。使用普通负载无法达到相同的效果(至少在 x86 上)。


2实际上并不像在循环中添加一个预取那么简单,因为在前五次迭代之后,负载将开始达到已经预取的值,从而将 MLP 减少回 5 - 但这个想法仍然成立。真正的实现还涉及重新组织循环,以便可以维持 MLP(例如,每隔几次迭代就将加载和预取“干扰”在一起)。