在我的程序中(用简单的C编写)我有一个结构,它保存准备通过矢量化(仅AVX)基数-2 2D快速傅立叶变换进行变换的数据。结构如下所示:
struct data {
double complex *data;
unsigned int width;
unsigned int height;
unsigned int stride;
};
现在我需要尽快从内存中加载数据。据我所知,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));
}
}
正如预期的那样,memcpy调用已经过优化。然而,在观察之后,gcc将memcpy转换为两个vmovupd指令或一堆movq指令,这些指令将数据加载到堆栈上保证对齐的位置,然后将两个vmovapd指令加载到ymm寄存器。此行为取决于是否定义了memcpy原型(如果已定义,则gcc使用movq和vmovapd)。
我能够确保内存中的数据是对齐的,但我不知道如何告诉gcc它只能使用movapd指令将数据从内存直接加载到ymm寄存器。我强烈怀疑gcc不知道&(data->data[i*stride+j])
指向的数据总是对齐的事实。
有没有选项如何告诉gcc指针指向的数据总是对齐?
答案 0 :(得分:8)
vmovupd
与vmovapd
完全一样快。唯一的区别是当数据未对齐时vmovapd
出现故障。 (请参阅x86代码wiki中的优化链接,尤其是Agner Fog's optimization and microarch pdfs和英特尔optimization manual。
如果它使用多条指令而不是一条指令,则只会出现问题。
由于您在_mm256_mul_pd
使用英特尔内在函数,使用加载/存储内在函数而不是memcpy!请参阅sse标记维基以获取内在函数指南等。
// 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);
}
如果您想要未对齐的加载/存储,请使用_mm256_loadu_pd
/ storeu
。
或者您可以将double complex*
投放到__m256d*
并直接取消引用。在GCC中,这相当于对齐负载内在函数。但通常的惯例是使用加载/存储内在函数。
要回答标题问题,你可以通过告诉它何时保证指针对齐来帮助gcc自动矢量化:
data = __builtin_assume_aligned(data, 64);
在C ++中,您需要转换结果,但在C void*
中可以自由转换。
请参阅How to tell GCC that a pointer argument is always double-word-aligned?和https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html。
这当然特定于GNU C / C ++方言(clang,gcc,icc),不能移植到MSVC或其他不支持GNU扩展的编译器。
到目前为止,我对阵列上的所有操作都采用了大致相似的结构。
多次循环数组通常比单次传递尽可能多。即使它在L1D中都保持热,但只需额外的加载和存储指令就会成为瓶颈,相比之下,当数据在寄存器中时,这样做会更多。
答案 1 :(得分:0)
正如奥拉夫指出的那样,可以编写适当的加载和保存功能。所以现在代码很好地转换为加载时的两个vmovapd指令和保存时的两个vmovapd。
static inline void mload(union m256d t[2], double complex *f)
{
t[0].reg = _mm256_load_pd((double *)f);
t[1].reg = _mm256_load_pd((double *)(f+2));
}
static inline void msave(union m256d f[2], double complex *t)
{
_mm256_store_pd((double *)t, f[0].reg);
_mm256_store_pd((double *)(t+2), f[1].reg);
}
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];
mload(a, &( data->data[i*stride+j]));
mload(b, &(filter->data[i*stride+j]));
r[0].reg = _mm256_mul_pd(a[0].reg, b[0].reg);
r[1].reg = _mm256_mul_pd(a[1].reg, b[1].reg);
msave(r, &(data->data[i*stride+j]));
}
}