在循环迭代之间使用整个缓存行有什么特别的好处吗?

Is there a special benefit to consuming whole cache lines between iterations of a loop?

提问人:Matt 提问时间:6/19/2022 最后编辑:Matt 更新时间:6/19/2022 访问量:197

问:

我的程序添加了浮点数组,并且在使用 MSVC 和 G++ 进行最大优化编译时展开了 4 倍。我不明白为什么两个编译器都选择展开 4x,所以我做了一些测试,发现只有偶尔在运行时进行 t 检验,用于手动展开 1 对 2 或 1 对 4 迭代,p 值 ~0.03,2-vs-4 很少< 0.05,2-vs-8+ 总是> 0.05。

如果我将编译器设置为使用 128 位向量或 256 位向量,它总是展开 4 倍,这是 64 字节缓存行的倍数(重要还是巧合?

我之所以考虑缓存行,是因为我没想到展开会对按顺序读取和写入千兆字节浮点数的内存绑定程序产生任何影响。在这种情况下,展开应该有好处吗?也有可能没有显着差异,我的样本量不够大。

我发现这篇博客说,对于中等大小的阵列,手动展开阵列副本的速度更快,而对于较长的阵列,流式处理速度更快。它们的 AvxAsyncPFCopier 和 AvxAsyncPFUnrollCopier 函数似乎受益于使用整个缓存行以及手动展开。在博客中进行基准测试,源代码在这里

#include <iostream>
#include <immintrin.h>

int main() {
    // example of manually unrolling float arrays
    size_t bytes = sizeof(__m256) * 10;
    size_t alignment = sizeof(__m256);
    // 10 x 32-byte vectors
    __m256* a = (__m256*) _mm_malloc(bytes, alignment); 
    __m256* b = (__m256*) _mm_malloc(bytes, alignment);
    __m256* c = (__m256*) _mm_malloc(bytes, alignment); 

    for (int i = 0; i < 10; i += 2) {
        // cache miss?
        // load 2 x 64-byte cache lines:
        //      2 x 32-byte vectors from b
        //      2 x 32-byte vectors from c
        a[i + 0] = _mm256_add_ps(b[i + 0], c[i + 0]);

        // cache hit?
        a[i + 1] = _mm256_add_ps(b[i + 1], c[i + 1]);

        // special bonus for consuming whole cache lines?
    }
}

3 个唯一浮点数组的原始源

for (int64_t i = 0; i < size; ++i) {
    a[i] = b[i] + c[i];
}

带有 AVX2 指令的 MSVC

            a[i] = b[i] + c[i];
00007FF7E2522370  vmovups     ymm2,ymmword ptr [rax+rcx]  
00007FF7E2522375  vmovups     ymm1,ymmword ptr [rcx+rax-20h]  
00007FF7E252237B  vaddps      ymm1,ymm1,ymmword ptr [rax-20h]  
00007FF7E2522380  vmovups     ymmword ptr [rdx+rax-20h],ymm1  
00007FF7E2522386  vaddps      ymm1,ymm2,ymmword ptr [rax]  
00007FF7E252238A  vmovups     ymm2,ymmword ptr [rcx+rax+20h]  
00007FF7E2522390  vmovups     ymmword ptr [rdx+rax],ymm1  
00007FF7E2522395  vaddps      ymm1,ymm2,ymmword ptr [rax+20h]  
00007FF7E252239A  vmovups     ymm2,ymmword ptr [rcx+rax+40h]  
00007FF7E25223A0  vmovups     ymmword ptr [rdx+rax+20h],ymm1  
00007FF7E25223A6  vaddps      ymm1,ymm2,ymmword ptr [rax+40h]  
00007FF7E25223AB  add         r9,20h  
00007FF7E25223AF  vmovups     ymmword ptr [rdx+rax+40h],ymm1  
00007FF7E25223B5  lea         rax,[rax+80h]  
00007FF7E25223BC  cmp         r9,r10  
00007FF7E25223BF  jle         main$omp$2+0E0h (07FF7E2522370h) 

具有默认指令的 MSVC

            a[i] = b[i] + c[i];
00007FF71ECB2372  movups      xmm0,xmmword ptr [rax-10h]  
00007FF71ECB2376  add         r9,10h  
00007FF71ECB237A  movups      xmm1,xmmword ptr [rcx+rax-10h]  
00007FF71ECB237F  movups      xmm2,xmmword ptr [rax+rcx]  
00007FF71ECB2383  addps       xmm1,xmm0  
00007FF71ECB2386  movups      xmm0,xmmword ptr [rax]  
00007FF71ECB2389  addps       xmm2,xmm0  
00007FF71ECB238C  movups      xmm0,xmmword ptr [rax+10h]  
00007FF71ECB2390  movups      xmmword ptr [rdx+rax-10h],xmm1  
00007FF71ECB2395  movups      xmm1,xmmword ptr [rcx+rax+10h]  
00007FF71ECB239A  movups      xmmword ptr [rdx+rax],xmm2  
00007FF71ECB239E  movups      xmm2,xmmword ptr [rcx+rax+20h]  
00007FF71ECB23A3  addps       xmm1,xmm0  
00007FF71ECB23A6  movups      xmm0,xmmword ptr [rax+20h]  
00007FF71ECB23AA  addps       xmm2,xmm0  
00007FF71ECB23AD  movups      xmmword ptr [rdx+rax+10h],xmm1  
00007FF71ECB23B2  movups      xmmword ptr [rdx+rax+20h],xmm2  
00007FF71ECB23B7  add         rax,40h  
00007FF71ECB23BB  cmp         r9,r10  
00007FF71ECB23BE  jle         main$omp$2+0D2h (07FF71ECB2372h)  
C++ ++ cpu-architecture simd cpu-cache

评论

2赞 Peter Cordes 6/19/2022
对于连续访问,我不认为这是展开有帮助的原因;即使数组地址 % 64 == 16 或 32,我也希望展开具有相同的好处,因此 64 个连续字节跨越 2 行。如果沿着矩阵的一列向下走,最好使用整个缓存行,这样您就不必在下一次(一组)列的下一次外部迭代中再次触摸这些行。
3赞 Goswin von Brederlow 6/19/2022
执行 AVX 加载比执行 4 个单独的寄存器加载更有效,即使内存受限也是如此。因此,编译器将展开循环,直到它可以执行组合的大负载,但除此之外,进一步展开没有任何好处。
1赞 Goswin von Brederlow 6/19/2022
你应该做一个并避免所有实现定义的东西。如果从结构中知道对齐方式,您的原始代码将很好地优化。struct alignas(64) Test { float f[16]; };_
1赞 Peter Cordes 6/19/2022
如果您在 DRAM 带宽上遇到瓶颈,那么错位(相对于矢量宽度)几乎没有任何区别,就像现代 AVX2 CPU 的几个百分点一样。(如果您像这里一样使用需要对齐的负载,则正确性除外,请使用 deref 代替 )。与 AVX-512 不同,AVX-512 的指针未对齐,即使对于 DRAM 也是如此,例如 15% 左右。(像 Sandybridge 这样的 AVX1 CPU 在未对齐的 256 位加载/存储时确实有更糟糕的减速,至少在缓存行拆分时是这样。但除此之外,DRAM的速度太慢了,以至于有时间吸收分行的惩罚。__m256*_mm256_loadu_ps
2赞 Peter Cordes 6/19/2022
按向量宽度对齐数组通常很好,不需要一直到 64 字节行或 128 字节行对。或 2MiB 大页面。

答: 暂无答案