如何告诉gcc指针指向的数据总是对齐?

Kos*_*ahb 5 c memory gcc avx

在我的程序中(用简单的C语言编写)我有一个结构,它保存准备通过矢量化(仅AVX)基数-2 2D快速傅立叶变换进行变换的数据.结构如下所示:

struct data {
    double complex *data;
    unsigned int width;
    unsigned int height;
    unsigned int stride;
};
Run Code Online (Sandbox Code Playgroud)

现在我需要尽快从内存加载数据.据我所知,ymm寄存器(vmovapd和vmovupd指令)存在未对齐和对齐的加载,我希望程序使用对齐版本更快.

到目前为止,我对阵列上的所有操作使用大致相似的结构.当数据和滤波器已经转换到频域并且滤波器通过逐元素乘法应用于数据时,该示例是程序的一部分.

union m256d {
    __m256d reg;
    double d[4];

};

struct data *data, *filter;
/* Load data and filter here, both have the same width, height and stride. */

unsigned int stride = data->stride;
for(unsigned int i = 0; i<data->height; i++) {
    for(unsigned int j = 0; j<data->width; j+=4) {
        union m256d a[2];
        union m256d b[2];
        union m256d r[2];

        memcpy(a, &(  data->data[i*stride+j]), 2*sizeof(*a));
        memcpy(b, &(filter->data[i*stride+j]), 2*sizeof(*b));

        r[0].reg = _mm256_mul_pd(a[0].reg, b[0].reg);
        r[1].reg = _mm256_mul_pd(a[1].reg, b[1].reg);

        memcpy(&(data->data[i*stride+j]), r, 2*sizeof(*r));
    }
}
Run Code Online (Sandbox Code Playgroud)

正如预期的那样,memcpy调用已经过优化 然而,在观察之后,gcc将memcpy转换为两个vmovupd指令或一堆movq指令,这些指令将数据加载到堆栈上保证对齐的位置,然后将两个vmovapd指令加载到ymm寄存器.此行为取决于是否定义了memcpy原型(如果已定义,则gcc使用movq和vmovapd).

我能够确保内存中的数据是对齐的,但我不知道如何告诉gcc它只能使用movapd指令将数据从内存直接加载到ymm寄存器.我强烈怀疑gcc不知道指向的数据&(data->data[i*stride+j])始终是对齐的.

有没有选项如何告诉gcc指针指向的数据总是对齐?

Pet*_*des 8

vmovupdvmovapd与数据在运行时实际对齐时的速度完全一样快.唯一的区别是vmovapd数据未对齐时出现故障.(请参阅标签wiki 中的优化链接,尤其是Agner Fog的优化和微格式pdf,以及英特尔的优化手册.

如果它使用多个指令而不是一个指令,则只会出现问题.


由于您使用的是英特尔内在函数_mm256_mul_pd,因此请使用加载/存储内在函数而不是memcpy! 有关内在指南的更多信息,请参阅标签wiki.

// Hoist this outside the loop,
// mostly for readability; should optimize fine either way.
// Probably only aliasing-safe to use these pointers with _mm256_load/store (which alias anything)
// unless C allows `double*` to alias `double complex*`
const double *flat_filt = (const double*)filter->data;
      double *flat_data =       (double*)data->data;

for (...) {
    //union m256d a[2];
    //union m256d b[2];
    //union m256d r[2];

       //memcpy(a, &(  data->data[i*stride+j]), 2*sizeof(*a));
    __m256d a0 = _mm256_load_pd(0 + &flat_data[i*stride+j]);
    __m256d a1 = _mm256_load_pd(4 + &flat_data[i*stride+j]);
       //memcpy(b, &(filter->data[i*stride+j]), 2*sizeof(*b));
    __m256d b0 = _mm256_load_pd(0 + &flat_filt[i*stride+j]);
    __m256d b1 = _mm256_load_pd(4 + &flat_filt[i*stride+j]);
       // +4 doubles = +32 bytes = 1 YMM vector = +2 double complex

    __m256d r0 = _mm256_mul_pd(a0, b0);
    __m256d r1 = _mm256_mul_pd(a1, b1);

       // memcpy(&(data->data[i*stride+j]), r, 2*sizeof(*r));
    _mm256_store_pd(0 + &flat_data[i*stride+j], r0);
    _mm256_store_pd(4 + &flat_data[i*stride+j], r1);
}
Run Code Online (Sandbox Code Playgroud)

如果你想要一个未对齐的加载/存储,你可以使用_mm256_loadu_pd/ storeu.

或者你可以直接将你转换double complex*为a __m256d*并取消引用.在GCC中,这相当于对齐负载内在函数.但通常的惯例是使用加载/存储内在函数.


但是,要回答标题问题,您可以通过告诉它何时保证指针对齐来帮助gcc自动向量化:

data = __builtin_assume_aligned(data, 64);
Run Code Online (Sandbox Code Playgroud)

在C++中,您需要转换结果,但在C中void*可以自由转换.

请参阅如何告诉GCC指针参数始终是双字对齐的?https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html.

这当然特定于GNU C/C++方言(clang,gcc,icc),不能移植到MSVC或其他不支持GNU扩展的编译器.


到目前为止,我对阵列上的所有操作使用大致相似的结构.

多次循环数组通常比单次传递尽可能多.即使它在L1D中都保持热,但只有额外的加载和存储指令是一个瓶颈,而在数据处于寄存器中的情况下执行更多操作.