我试图通过以等于2的幂的步幅访问内存来演示Intel CPU上的缓存关联性。 我以列方式(在C ++中)访问一个双精度的K x N矩阵,并期望对于K> 8,如果N是2的幂,性能将会下降(因为我使用的是8路CPU设置关联的L1缓存)。因此,我绘制了K = 1..40,N = 2 ^ 20,N = 2 ^ 20 + 8和N = 2 ^ 20 + 64的内存访问性能,从而填充了0、1和8个缓存行分别。
#include <cstdio>
#include <cstdlib>
#include <chrono>
double buffer[((1 << 20) + 64) * 40];
void measure_flops(int N, int K) {
// Warm-up.
for (int j = 0; j < 10; ++j)
for (int i = 0; i < N * K; ++i)
buffer[i] += 1e-10 * i + 0.123 * j;
// Iterate.
int repeat = 500 / K;
auto start = std::chrono::steady_clock::now();
for (int r = 0; r < repeat; ++r)
for (int i = 0; i < N; ++i)
for (int j = 0; j < K; ++j)
buffer[j * N + i] += 1.0123;
auto end = std::chrono::steady_clock::now();
std::chrono::duration<double> diff = end - start;
volatile double tmp = buffer[rand() % (N * K)]; (void)tmp;
// Report.
double flops = (double)repeat * N * K / diff.count();
printf("%d %2d %.4lf\n", N, K, flops * 1e-9);
fflush(stdout);
}
void run(int N) {
printf(" N K GFLOPS\n");
for (int K = 1; K <= 40; ++K)
measure_flops(N, K);
printf("\n\n");
}
int main() {
const int N = 1 << 20;
run(N);
run(N + 64 / sizeof(double));
run(N + 512 / sizeof(double));
return 0;
}
我在以下CPU上运行基准测试:
...结果不一致,某些形状我不明白。
1)为什么对于E5-2680,对于padding = 64 B,性能不能恢复,如果对于E5-2670,性能不能恢复?对于padding = 512 B,性能会恢复,但是为什么呢? (预取器是否有可能破坏缓存?)
编辑:在另一个群集上也在E5-2690和E5-2650上进行了测试。 E5-2690表现为E5-2680(填充= 64 B时性能不佳),而E5-2650表现为E5-2670(性能良好)。因此,似乎确实在2670和2680之间发生了一些变化。
2)为什么性能在K达到8之前这么早下降? (我怀疑这是“伪造的商店转发摊位”,如在here稍有不同的设置中所述,但我不确定。)
3)为什么为什么E5-2680的K =〜32和Gold 6150的K =〜35再次降低性能?
(perf stat -ddd
显示在那里增加了L3缓存未命中的数量,但是我不知道为什么的确切原因,因为K与L3关联性不匹配。)
E5-2680和Gold 6150都在群集上运行,在该群集中,我采用一个完整的节点来确保不会干扰其他用户。在这两种情况下,一个节点都包含2个CPU,但无论如何我都只使用一个内核。 E5-2670是在单CPU共享计算机上(无法获得完整节点),但是在非活动状态下运行了基准测试。
我用g++ -O1 -std=c++11
(E5-2680的版本6.3.0和Gold,E5-2670的版本7.3.0)编译了代码。所有结果都是可重复的。更改编译器(或例如向-O3
添加volatile
或向buffer
添加{{1}})只会或多或少地导致低K的结果,例如K = 2变快/变慢。