某些CPU和编译器提供预取指令.例如:GCC文档中的 __builtin_prefetch .虽然GCC的文件中有评论,但它对我来说太短了.
我想知道,在prantice中,我们应该何时使用预取?有一些例子吗?谢谢!
在最近的英特尔芯片上,您显然可能想要使用预取的一个原因是避免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相反,"关闭"测试的减速部分延续到"开启"测试,尽管效果不那么极端,可能是因为节省的"加速"行为比"减速"行为更快.
这个问题并不是关于编译器的,因为它们只是提供一些钩子来将预取指令插入到汇编代码/二进制文件中.不同的编译器可能提供不同的内在格式,但您可以忽略所有这些并且(小心地)将它直接添加到汇编代码中.
现在真正的问题似乎是"什么时候预取有用",答案是 - 在任何情况下你都会受到内存延迟的限制,并且访问模式不规则且可区分用于捕获的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开销,而不是强调内存子系统(这就是为什么矢量帮助这么多).