Performance 哈斯韦尔存储器存取
我在试验AVX-AVX2指令集,以查看连续阵列上的流式处理性能。所以我有下面的例子,在这里我做基本的内存读取和存储Performance 哈斯韦尔存储器存取,performance,x86,cpu-architecture,avx2,intel-pmu,Performance,X86,Cpu Architecture,Avx2,Intel Pmu,我在试验AVX-AVX2指令集,以查看连续阵列上的流式处理性能。所以我有下面的例子,在这里我做基本的内存读取和存储 #include <iostream> #include <string.h> #include <immintrin.h> #include <chrono> const uint64_t BENCHMARK_SIZE = 5000; typedef struct alignas(32) data_t { double a[B
#include <iostream>
#include <string.h>
#include <immintrin.h>
#include <chrono>
const uint64_t BENCHMARK_SIZE = 5000;
typedef struct alignas(32) data_t {
double a[BENCHMARK_SIZE];
double c[BENCHMARK_SIZE];
alignas(32) double b[BENCHMARK_SIZE];
}
data;
int main() {
data myData;
memset(&myData, 0, sizeof(data_t));
auto start = std::chrono::high_resolution_clock::now();
for (auto i = 0; i < std::micro::den; i++) {
for (uint64_t i = 0; i < BENCHMARK_SIZE; i += 1) {
myData.b[i] = myData.a[i] + 1;
}
}
auto end = std::chrono::high_resolution_clock::now();
std::cout << (end - start).count() / std::micro::den << " " << myData.b[1]
<< std::endl;
}
所以我的问题是,为什么会发生这种影响,考虑到haswell应该能够提供2*32字节的读取,并且每个周期存储32字节
编辑1
我用这段代码意识到gcc巧妙地消除了对myData.a的访问,因为它被设置为0。为了避免这种情况,我做了另一个略有不同的基准测试,其中显式地设置了
#include <iostream>
#include <string.h>
#include <immintrin.h>
#include <chrono>
const uint64_t BENCHMARK_SIZE = 4000;
typedef struct alignas(64) data_t {
double a[BENCHMARK_SIZE];
alignas(32) double c[BENCHMARK_SIZE];
alignas(32) double b[BENCHMARK_SIZE];
}
data;
int main() {
data myData;
memset(&myData, 0, sizeof(data_t));
std::cout << sizeof(data) << std::endl;
std::cout << sizeof(myData.a) << " cache lines " << sizeof(myData.a) / 64
<< std::endl;
for (uint64_t i = 0; i < BENCHMARK_SIZE; i += 1) {
myData.b[i] = 0;
myData.a[i] = 1;
myData.c[i] = 2;
}
auto start = std::chrono::high_resolution_clock::now();
for (auto i = 0; i < std::micro::den; i++) {
for (uint64_t i = 0; i < BENCHMARK_SIZE; i += 1) {
myData.b[i] = myData.a[i] + 1;
}
}
auto end = std::chrono::high_resolution_clock::now();
std::cout << (end - start).count() / std::micro::den << " " << myData.b[1]
<< std::endl;
}
答案中也指出了同样的模式,随着
数据集大小数据不再适合L1,L2成为瓶颈。是什么
另一个有趣的是,预取似乎没有帮助,而且L1未命中
大幅增加。尽管如此,我希望看到至少50%的命中率,因为每个带到一级进行读取的缓存线都会命中
对于第二次访问(每次迭代读取64字节缓存线32字节)。然而,一旦数据集溢出到二级,一级命中率似乎下降到2%。考虑到阵列实际上与一级缓存大小不重叠,这不应该是因为缓存冲突。所以这部分对我来说仍然没有意义。执行摘要:
对于相同的基本工作负载,不同的缓存级别可以支持不同的峰值带宽,因此具有不同大小的数据集会极大地影响性能 详细解释:
考虑到哈斯韦尔(Haswell)的情况,这并不奇怪,比如说,他可以 每循环维持2次负载和1次存储 但这只是说申请L1。如果你继续读下去,你会发现L2 可以为每个周期的数据或指令缓存提供完整的64B行 由于每次迭代需要一个加载和一个存储,因此让数据集驻留在L1中将允许您享受L1带宽并可能达到每次迭代的周期吞吐量,而让数据集溢出到L2将迫使您等待更长的时间。这取决于系统中double的大小,但由于它通常是8字节,4000*2个数组*8字节=64k,这超过了大多数当前系统上的L1大小。然而,Peter Cords在评论中指出,原始代码可能优化了零数据阵列(我不确信,但这是可能的) 现在,一旦开始进入下一个缓存级别,就会发生两件事:
编辑1(以下为您的) 消除阵列的一大亮点,它解决了双倍大小的谜团——它确实是64位的,因此一个4000元素的阵列,或者两个2000元素的阵列(修复后)都可以在L1中容纳。现在溢出发生在3000个元素处。L1命中率现在很低,因为L1无法发出足够的预回迁以在2个不同流之前运行 至于每个加载都会在2次迭代中产生一个64字节的行的预期-我看到了一些非常有趣的事情-如果你将从内存单元发出的加载数相加(L1命中+L1未命中),你会发现2000个元素的情况几乎是1000个元素的2倍,但这3000例和4000例病例并非分别是3倍和4倍,而是一半。具体来说,每个数组有3000个元素,访问量比2000个元素少
这使我怀疑内存单元能够将每2个加载合并到一个内存访问中,但只有在进入L2和更高级别时。这是有道理的,当你想到它,没有r
#include <iostream>
#include <string.h>
#include <immintrin.h>
#include <chrono>
const uint64_t BENCHMARK_SIZE = 4000;
typedef struct alignas(64) data_t {
double a[BENCHMARK_SIZE];
alignas(32) double c[BENCHMARK_SIZE];
alignas(32) double b[BENCHMARK_SIZE];
}
data;
int main() {
data myData;
memset(&myData, 0, sizeof(data_t));
std::cout << sizeof(data) << std::endl;
std::cout << sizeof(myData.a) << " cache lines " << sizeof(myData.a) / 64
<< std::endl;
for (uint64_t i = 0; i < BENCHMARK_SIZE; i += 1) {
myData.b[i] = 0;
myData.a[i] = 1;
myData.c[i] = 2;
}
auto start = std::chrono::high_resolution_clock::now();
for (auto i = 0; i < std::micro::den; i++) {
for (uint64_t i = 0; i < BENCHMARK_SIZE; i += 1) {
myData.b[i] = myData.a[i] + 1;
}
}
auto end = std::chrono::high_resolution_clock::now();
std::cout << (end - start).count() / std::micro::den << " " << myData.b[1]
<< std::endl;
}
| Event | Size=1000 | Size=2000 | Size=3000 | Size=4000 |
|----------------+-------------+-------------+-------------+---------------|
| Time | 86 ns | 166 ns | 734 ns | 931 ns |
| L1 load hit | 252,807,410 | 494,765,803 | 9,335,692 | 9,878,121 |
| L1 load miss | 24,931 | 585,891 | 370,834,983 | 495,678,895 |
| L2 load hit | 16,274 | 361,196 | 371,128,643 | 495,554,002 |
| L2 load miss | 9,589 | 11,586 | 18,240 | 40,147 |
| L1D wb acc. L2 | 9,121 | 771,073 | 374,957,848 | 500,066,160 |
| L1D repl. | 19,335 | 1,834,100 | 751,189,826 | 1,000,053,544 |
4007a8: 48 8d 85 d0 2a fe ff lea -0x1d530(%rbp),%rax
4007af: 90 nop
4007b0: c5 f5 58 00 vaddpd (%rax),%ymm1,%ymm0
4007b4: 48 83 c0 20 add $0x20,%rax
4007b8: c5 fd 29 80 60 38 01 vmovapd %ymm0,0x13860(%rax)
4007bf: 00
4007c0: 48 39 c2 cmp %rax,%rdx
4007c3: 75 eb jne 4007b0 <main+0x50>
4007c5: 83 e9 01 sub $0x1,%ecx
4007c8: 75 de jne 4007a8 <main+0x48>