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

 墨镜小辣妹 发布于 2023-02-09 07:28

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

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

2 个回答
  • 在最近的英特尔芯片上,您显然可能想要使用预取的一个原因是避免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
    

    在预测数字的情况下,预取可以达到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
    

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

    调查

    事实上,如果你改变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
    

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

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

    让我们来看看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     
    

    ......而版本:

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

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

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

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

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

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

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

    请注意,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
    

    编译标志: -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版本上的矢量化.

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

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


    2023-02-09 07:40 回答
撰写答案
今天,你开发时遇到什么问题呢?
立即提问
热门标签
PHP1.CN | 中国最专业的PHP中文社区 | PNG素材下载 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有