处理双数组的未对齐部分,向量化其余部分

hr0*_*r0m 9 c c++ x86 sse vectorization

我正在生成sse/avx指令,目前我必须使用未对齐的加载和存储.我在浮点/双数组上操作,我永远不知道它是否会对齐.所以在矢量化它之前,我希望有一个pre和可能的post循环,它关注未对齐的部分.然后,主矢量化循环在对齐的部分上操作.

但是我如何确定阵列何时对齐?我可以查看指针值吗?应该何时预循环停止和循环后启动?

这是我的简单代码示例:

void func(double * in, double * out, unsigned int size){
    for( as long as in unaligned part ){
        out[i] = do_something_with_array(in[i])
    }
    for( as long as aligned ){
        awesome avx code that loads operates and stores 4 doubles
    }
    for( remaining part of array ){
        out[i] = do_something_with_array(in[i])
    }
 }
Run Code Online (Sandbox Code Playgroud)

编辑:我一直在考虑它.从理论上讲,指向第i个元素的指针应该是可分的(类似于&a [i]%16 == 0)2,4,16,32(取决于它是否是双倍以及它是sse还是avx).所以第一个循环应该掩盖不可分割的元素.

实际上我将尝试编译器编译指示和标志输出,以查看编译器产生了什么.如果没有人给出一个好的答案,我会在周末发布我的解决方案(如果有的话).

Z b*_*son 6

下面是一些示例C代码,它可以满足您的需求

#include <stdio.h>
#include <x86intrin.h>
#include <inttypes.h>

#define ALIGN 32
#define SIMD_WIDTH (ALIGN/sizeof(double))

int main(void) {
    int n = 17;
    int c = 1;
    double* p = _mm_malloc((n+c) * sizeof *p, ALIGN);
    double* p1 = p+c;
    for(int i=0; i<n; i++) p1[i] = 1.0*i;
    double* p2 = (double*)((uintptr_t)(p1+SIMD_WIDTH-1)&-ALIGN);
    double* p3 = (double*)((uintptr_t)(p1+n)&-ALIGN);
    if(p2>p3) p2 = p3;

    printf("%p %p %p %p\n", p1, p2, p3, p1+n);
    double *t;
    for(t=p1; t<p2; t+=1) {
        printf("a %p %f\n", t, *t);
    }
    puts("");
    for(;t<p3; t+=SIMD_WIDTH) {
        printf("b %p ", t);
        for(int i=0; i<SIMD_WIDTH; i++) printf("%f ", *(t+i));
        puts("");
    }
    puts("");
    for(;t<p1+n; t+=1) {
        printf("c %p %f\n", t, *t);
    }  
}
Run Code Online (Sandbox Code Playgroud)

这将生成一个32字节的对齐缓冲区,但随后将其偏移一个大小的两倍,因此它不再是32字节对齐的.它循环遍历标量值,直到达到32-btye对齐,循环遍历32字节对齐值,然后最后用另一个标量循环完成任何不是SIMD宽度倍数的剩余值.


我认为这种优化只对Nehalem之前的英特尔x86处理器有很大意义.由于Nehalem,未对齐的加载和存储的延迟和吞吐量与对齐的加载和存储相同.此外,由于Nehalem缓存线分割的成本很小.

自Nehalem以来,SSE有一个微妙之处在于未对齐的载荷和存储不能与其他操作折叠.因此,自Nehalem以来,对齐的装载和存储不会因SSE而过时.所以原则上这种优化甚至可以与Nehalem产生差异,但在实践中我认为很少会出现这样的情况.

但是,对于AVX未对齐的加载和存储可以折叠,因此对齐的加载和存储指令已过时.


我用GCC,MSVC和Clang调查了这个.GCC如果它不能假设指针与SSE对齐例如16个字节那么它将生成类似于上面代码的代码以达到16字节对齐以避免在向量化时高速缓存行分裂.

Clang和MSVC不会这样做,因此它们会受到缓存行分裂的影响.但是,执行此操作的附加代码的成本弥补了缓存行拆分的成本,这可能解释了为什么Clang和MSVC不担心它.

唯一的例外是纳哈莱姆之前.在这种情况下,当指针未对齐时,GCC比Clang和MSVC快得多.如果指针对齐且Clang知道它,那么它将使用对齐的加载和存储,并且像GCC一样快.MSVC矢量化仍然使用未对齐的存储和加载,因此即使指针是16字节对齐,因此在Nahalem之前也很慢.


这是一个我认为使用指针差异更清晰的版本

#include <stdio.h>
#include <x86intrin.h>
#include <inttypes.h>

#define ALIGN 32
#define SIMD_WIDTH (ALIGN/sizeof(double))

int main(void) {
    int n = 17, c =1;

    double* p = _mm_malloc((n+c) * sizeof *p, ALIGN);
    double* p1 = p+c;
    for(int i=0; i<n; i++) p1[i] = 1.0*i;
    double* p2 = (double*)((uintptr_t)(p1+SIMD_WIDTH-1)&-ALIGN);
    double* p3 = (double*)((uintptr_t)(p1+n)&-ALIGN);
    int n1 = p2-p1, n2 = p3-p2;
    if(n1>n2) n1=n2;
    printf("%d %d %d\n", n1, n2, n);

    int i;
    for(i=0; i<n1; i++) {
        printf("a %p %f\n", &p1[i], p1[i]);
    }
    puts("");
    for(;i<n2; i+=SIMD_WIDTH) {
        printf("b %p ", &p1[i]);
        for(int j=0; j<SIMD_WIDTH; j++) printf("%f ", p1[i+j]);
        puts("");
    }
    puts("");
    for(;i<n; i++) {
        printf("c %p %f\n", &p1[i], p1[i]);
    }  
}
Run Code Online (Sandbox Code Playgroud)