Performance 我们什么时候应该使用预取?
一些CPU和编译器提供预取指令。例如:内置预取。虽然GCC的文档中有一条评论,但对我来说太短了Performance 我们什么时候应该使用预取?,performance,x86,arm,prefetch,Performance,X86,Arm,Prefetch,一些CPU和编译器提供预取指令。例如:内置预取。虽然GCC的文档中有一条评论,但对我来说太短了 我想知道,在prantice中,我们什么时候应该使用预取?有一些例子吗?谢谢 看来,最好的策略是根本不使用内置预取(及其朋友,内置预取)。在某些平台上,这些可能会有所帮助(甚至帮助很大)——但是,必须始终进行一些基准测试来确认这一点。真正的问题是,从长远来看,短期业绩增长是否值得付出代价 首先,有人可能会问以下问题:当这些语句被馈送到更高端的现代CPU时,它们实际上做了什么?答案是:没有人真正知道(除
我想知道,在prantice中,我们什么时候应该使用预取?有一些例子吗?谢谢 看来,最好的策略是根本不使用内置预取(及其朋友,内置预取)。在某些平台上,这些可能会有所帮助(甚至帮助很大)——但是,必须始终进行一些基准测试来确认这一点。真正的问题是,从长远来看,短期业绩增长是否值得付出代价 首先,有人可能会问以下问题:当这些语句被馈送到更高端的现代CPU时,它们实际上做了什么?答案是:没有人真正知道(除了,可能是CPU核心架构团队中的一些人,但他们不会告诉任何人)。现代CPU是非常复杂的机器,能够对指令进行重新排序,在可能未执行的分支上推测执行指令,等等。此外,这种复杂行为的细节可能(也将)在不同的CPU代和供应商之间有很大的不同(Intel Core vs Intel I*vs AMD Opteron;对于ARM等更为分散的平台,情况更糟)
这里列出了一个简洁的CPU功能示例(与预取无关,但仍与预取相关),该示例用于在较旧的Intel CPU上加速,但在较现代的CPU上却表现不佳:。在这种情况下,通过使用显式(“naive”)替换gcc提供的优化版memcmp,可以实现18%的性能提升也就是说)循环。这个问题实际上与编译器无关,因为它们只是提供了一些钩子来将预取指令插入汇编代码/二进制文件。不同的编译器可能提供不同的内在格式,但您可以忽略所有这些,然后(小心地)直接将其添加到汇编代码中 现在真正的问题似乎是“预回迁什么时候有用”,答案是——在任何情况下,如果内存延迟有限,访问模式不规则,硬件预回迁无法捕获(以流或步幅组织),或当您怀疑硬件无法同时跟踪太多不同的流时。
大多数编译器很少为您插入自己的预回迁,所以基本上由您来处理代码并对预回迁的有用性进行基准测试 @Mystical的链接展示了一个很好的例子,但这里有一个更直接的例子,我认为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);
}
请注意,即使我使控制流更加复杂(额外循环嵌套级别),也会收到加速,分支预测器应该可以轻松捕获短块大小循环的模式,并且它可以避免执行不必要的预取
请注意,Ivybridge和更高版本将继续,因此硬件可能能够在这些CPU上缓解这种情况(如果有人有一个可用的CPU并且愿意尝试,我很乐意知道)。在这种情况下,我会修改基准测试,使其每第二行求和(并且每次预取都会向前看两行),这将混淆硬件预取程序
天湖结果
以下是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
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
编译标志:-O2-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
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
时,没有预取的循环是矢量化的,但预取的额外复杂性variant循环阻止在我的gcc版本上进行矢量化
因此,-O2
的结果可能是苹果对苹果的结果,其好处大约是我们在Leeor的Westmile上看到的一半(8%的加速比16%)。但值得注意的是,您必须小心不要更改代码生成,从而导致大幅减速
这个测试可能并不理想,因为使用int
byint
意味着大量的CPU开销,而不是强调内存子系统(这就是为什么矢量化帮助很大)
这篇文章“每个程序员都应该知道关于内存的知识 Ulrich Drepper’讨论了预取有利的情况; ,警告:这是一篇相当长的文章,讨论了内存体系结构/cpu如何工作等问题 如果数据与缓存线对齐,并且如果您正在加载算法即将访问的数据,则预取会提供一些信息
在任何情况下,在尝试优化高度使用的代码时都应该这样做;基准测试是必须的,而且结果通常与人们想象的不同。在最近的英特尔芯片上,您显然希望使用预取的一个原因是避免人为限制已实现内存带宽的CPU节能功能th。在这种情况下,与不进行预取的相同代码相比,简单预取可以将性能提高一倍,但这完全取决于所选的电源管理计划 我在中运行了一个简化版(代码)的测试,它对内存子系统的压力更大(因为预取在这方面会有所帮助、有所伤害或什么都不做)。最初的测试强调CPU与内存子系统并行,因为它将每个缓存线上的每个
int
相加。由于典型的内存读取带宽在15 GB/s的范围内,即每秒37.5亿个整数,因此最大速度的上限相当高(未矢量化的代码通常每个周期处理1int
或更少,因此3.75 GHz CPU的CPU和内存容量大致相同)
首先,我得到的结果似乎令人满意
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
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
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