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

时间:2013-12-20 05:54:34

标签: performance x86 arm prefetch

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

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

6 个答案:

答案 0 :(得分:7)

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

现在真正的问题似乎是“什么时候预取有用”,答案是 - 在任何情况下,你的内存延迟都是限制的,并且访问模式不规则且可区分用于捕获的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); 
}

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

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

Prefetching off: N=4096, sum=3355443200, time=1839
Prefetching on:  N=4096, sum=3355443200, time=1502

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

请注意Ivybridge并在should have a "next-page prefetcher"上继续,因此硬件可能能够在这些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

编译标记: -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

因此,使用预取大约是40%更慢,或者快8%,具体取决于您是否分别对此特定示例使用-O3-O2-O3的大幅减速实际上是由于代码生成的怪癖:在-O3没有预取的循环被矢量化,但预取变量循环的额外复杂性无论如何都会阻止我的gcc版本上的矢量化。 / p>

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

这个测试可能并不理想,因为int通过int意味着需要大量的CPU开销而不是强调内存子系统(这就是为什么矢量化帮助了很多)。


答案 1 :(得分:5)

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

我在here中运行了测试的简化版本(代码Leeor's answer),这更加强调了内存子系统(因为预取将帮助,伤害或什么都不做) )。最初的测试强调CPU与内存子系统并行,因为它在每个缓存行上每int加在一起。由于典型的内存读取带宽在15 GB / s的范围内,即每秒37.5亿个整数,因此对最大速度设置了相当大的限制(未经过矢量化的代码通常会处理1 {{每个周期1}}或更少,因此3.75 GHz CPU将大致相同的CPU和内存数量。)

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

int

对数字进行注视,预取可以达到16 GiB / s以上,而不仅仅是12.5,因此预取会使速度提高约30%。正确?

没那么快。记住省电模式在现代芯片上有各种各样的精彩交互,我将我的Linux CPU调控器从 powersave 1 的默认值改为 performance 。现在我明白了:

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

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

研究

事实上,如果你改变benchark,预取与否之间的区别就更加极端了。现有的基准测试在预取和打开之间交替运行,结果证明这有助于&#34;关闭&#34;因为速度增加发生在&#34; on&#34;试验部分延续到随后的试验 2 。如果您只运行 &#34; off&#34;测试你得到9 GiB / s左右的结果:

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

...对于预取版本约为17 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

因此预取版本几乎快两倍

让我们看一下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 发生了什么,对于** off *版本:

&#39; ./ prefetch-test off&#39;的性能计数器统计信息:

perf stat

...以及 on 版本:

   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     

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

现在we've seen this before,它可能是近期英特尔芯片上节能Turbo 功能的结果,它们在确定进程主要是内存时试图降低CPU频率大概是因为增加的CPU核心速度在那些情况下并没有提供很多好处。正如我们在这里看到的那样,这个假设并不总是正确的,但是如果权衡一般是一个坏的,或者启发式只是偶尔会错误的话,我也不清楚。

1 我正在运行intel_pstate驱动程序,这是最新内核上的Intel芯片的默认设置,它实现了&#34;硬件p-states&#34;被称为&#34; HWP&#34;。使用的命令: 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

2 相反,&#34; off&#34;测试部分延续到&#34; on&#34;测试,虽然效果不那么极端,可能是因为节能和#34;提升&#34;行为快于&#34;减速&#34;。

答案 2 :(得分:2)

这里是我所知道的情况的简短摘要,其中预取软件可能特别有用。有些可能并不适用于所有硬件。

应该从可以使用最明显位置软件预取的角度来阅读此列表,这是可以在软件中预测访问流的位置,但是这种情况并不一定对于SW预取而言,这是显而易见的胜利,因为乱序处理通常会产生相似的效果,因为乱序处理可以在现有未命中之后执行以获取更多未命中的命。

因此,此列表更像是“考虑到SW预取并不像它最初看起来那样明显有用,在某些地方无论如何它仍然有用”,通常将其与只是让乱序处理来完成它的工作,或者只是使用“普通加载”在需要它们之前加载一些值。

在乱序窗口中容纳更多负载

尽管乱序处理可能会潜在地公开与软件预取相同类型的MLP(内存级并行),但是在缓存未命中后,总可能的超前距离存在固有的限制。这些包括重排序缓冲区容量,负载缓冲区容量,调度程序容量等。 See this blog post的示例说明了额外的工作严重阻碍了MLP,因为CPU的运行速度不够快,无法立即执行足够的负载。

在这种情况下,软件预取使您可以在指令流中更早地有效填充更多负载。举例来说,假设您有一个循环,该循环执行一次加载,然后对已加载的数据执行20条指令的工作,并且您的CPU有100条指令的乱序缓冲区,并且加载彼此独立(例如, 。以已知的步幅访问数组。

第一次未命中后,您可以再运行99条指令,其中包括95条空载和5条装载指令(包括第一次装载)。因此,无序缓冲区的大小将MLP本质上限制为5。如果相反,您将每个加载与两个软件预取配对到一个位置,比如说要进行6次或更多次迭代,那么最终您将得到90条非加载指令,5个加载和5个软件预取,并且由于所有这些加载只是使MLP翻了一番到10 2

当然,每个负载没有一个额外的预取的限制:您可以添加更多以达到更高的数量,但是当您达到计算机的MLP限制并且预取占用时,有一个递减点,然后是负收益。您宁愿花在其他事情上的资源。

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

有序机器

并不是所有的机器都是乱序的内核:顺序CPU在某些地方(尤其是x86以外)仍然很常见,并且您还会发现“弱”的乱序内核没有能够向前运行很远,因此在某种程度上就像有序机器一样。

在这些机器上,软件预取可能会帮助您获得否则无法访问的MLP(当然,有序机器可能不支持很多固有的MLP)。

解决硬件预取限制

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

例如,Leeor's answer给出了在页面边界处停止硬件预取的示例,而软件预取没有任何此类限制。

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

后者的示例包括预取不连续的区域:例如较大矩阵的子矩阵中的行:硬件预取不会理解“矩形”区域的边界,并且会不断地预取每行的末尾,然后花一些时间来挑选新的行模式。软件预取可以完全正确地做到这一点:根本不会发出任何无用的预取(但通常需要对循环进行难看的拆分)。

如果您进行了足够的软件预取,则从理论上讲,应该关闭硬件优先技术,因为内存子系统的活动是他们用来决定是否激活的一种试探法。

对等点

在这里我应该注意,在可能加快速度的情况下,软件预取与硬件预取不等同,因为硬件预取可以加快速度:硬件预取可以更快。那是因为硬件预取可以开始于更接近存储器的工作(例如,从L2开始),在那里它对存储器的等待时间较短,并且还可以访问更多的缓冲区(在英特尔芯片上称为“超级队列”),因此并发性更高。因此,如果您关闭硬件预取功能并尝试通过纯软件预取功能来实现memcpy或其他流负载,您会发现它的速度可能会变慢。

特殊负载提示

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


2 实际上,这并不像在循环中添加单个预取那样简单,因为在前五次迭代之后,负载将开始达到已经预取的值,从而将MLP降低至5-这个想法仍然成立。实际的实现还涉及重新组织循环,以使MLP得以维持(例如,每几次迭代都会“阻塞”负载并预取在一起)。

答案 3 :(得分:0)

文章'每位程序员应该了解的内存 Ulrich Drepper讨论了预取有利的情况; http://www.akkadia.org/drepper/cpumemory.pdf,警告:这是一篇相当长的文章,讨论内存架构/ cpu如何工作等等。

如果数据与缓存行对齐,则

预取会产生一些内容;如果您正在加载即将被算法访问的数据;

无论如何,在尝试优化高度使用的代码时,应该这样做;基准测试是必须的,事情通常与人们想象的不同。

答案 4 :(得分:0)

在某些情况下,软件预取可以显着提高性能。

例如,如果您要访问速度相对较慢的存储设备(例如Optane DC持久性内存),而该设备的访问时间为几百纳秒,那么如果可以事先进行足够的预取,则预取可以将有效延迟减少50%或更多。的读写。

目前这不是很常见的情况,但是如果这种存储设备成为主流,它将变得更加普遍。

答案 5 :(得分:-1)

看来,遵循的最佳政策是永远不要使用__builtin_prefetch(及其朋友,__ builtin_expect)。在某些平台上,这些可能有所帮助(甚至可以帮助很多) - 但是,必须始终做一些基准测试来确认这一点。真正的问题是,从长期来看,短期业绩增长是否值得给它带来麻烦。

首先,人们可能会问以下问题:这些陈述在送到更高端的现代CPU时实际上做了什么?答案是:没有人真正知道(除了,可能是,CPU的核心架构团队中很少有人,但他们不会告诉任何人)。现代CPU是非常复杂的机器,能够进行指令重新排序,可能不采取分支的指令的推测性执行等等。此外,这种复杂行为的细节可能(并且将)在CPU代和供应商之间有很大差异(英特尔酷睿) vs Intel I * vs AMD Opteron;像ARM这样更加分散的平台,情况更糟糕。

一个简洁的例子(不是预取相关的,但仍然是)的CPU功能,用于加速旧的Intel CPU,但更糟糕的是更现代的一个在这里概述:http://lists-archives.com/git/744742-git-gc-speed-it-up-by-18-via-faster-hash-comparisons.html。在这种特殊情况下,通过用明确的(“天真的”所谓的)循环替换gcc提供的memcmp的优化版本,可以实现18%的性能提升。