如何告诉 gcc 指针指向的数据总是对齐的?
How to tell gcc that the data pointed to by a pointer will always be aligned?
在我的程序(用普通 C 编写)中,我有一个结构,它包含准备通过矢量化(仅限 AVX)radix-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 指针指向的数据将始终对齐?
当数据实际上在运行时对齐时,vmovupd
与 vmovapd
一样快。唯一的区别是 vmovapd
在数据未对齐时出错。 (请参阅 x86 tag wiki, especially Agner Fog's optimization and microarch pdfs, and Intel's optimization manual 中的优化链接。
只有当它使用多条指令而不是一条指令时才会出现问题。
由于您正在为 _mm256_mul_pd
使用 Intel 内在函数,使用 load/store 内在函数而不是 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);
}
如果您想要未对齐的 load/store,您可以使用 _mm256_loadu_pd
/ storeu
。
或者您可以将 double complex*
转换为 __m256d*
并直接取消引用。在 GCC 中,这相当于 aligned-load 内在函数。但通常的约定是使用 load/store 内在函数。
不过,要回答标题问题,您可以通过告诉 gcc auto-vectorize 何时保证指针对齐来帮助它:
data = __builtin_assume_aligned(data, 64);
在 C++ 中,您需要转换结果,但在 C 中 void*
可以自由转换。
参见 How to tell GCC that a pointer argument is always double-word-aligned? and https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html。
这当然是特定于 GNU C/C++ 方言(clang、gcc、icc)的,不能移植到 MSVC 或其他不支持 GNU 扩展的编译器。
So far I use roughly similar construction for all operations over the array.
多次遍历数组通常比一次遍历尽可能多的循环更糟糕。即使它在 L1D 中仍然很热,与数据在寄存器中时执行更多操作相比,额外的加载和存储指令也是一个瓶颈。
正如 Olaf 指出的那样,可以编写适当的加载和保存函数。所以现在代码很好地转换为加载时的两个 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]));
}
}
在我的程序(用普通 C 编写)中,我有一个结构,它包含准备通过矢量化(仅限 AVX)radix-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 指针指向的数据将始终对齐?
vmovupd
与 vmovapd
一样快。唯一的区别是 vmovapd
在数据未对齐时出错。 (请参阅 x86 tag wiki, especially Agner Fog's optimization and microarch pdfs, and Intel's optimization manual 中的优化链接。
只有当它使用多条指令而不是一条指令时才会出现问题。
由于您正在为 _mm256_mul_pd
使用 Intel 内在函数,使用 load/store 内在函数而不是 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);
}
如果您想要未对齐的 load/store,您可以使用 _mm256_loadu_pd
/ storeu
。
或者您可以将 double complex*
转换为 __m256d*
并直接取消引用。在 GCC 中,这相当于 aligned-load 内在函数。但通常的约定是使用 load/store 内在函数。
不过,要回答标题问题,您可以通过告诉 gcc auto-vectorize 何时保证指针对齐来帮助它:
data = __builtin_assume_aligned(data, 64);
在 C++ 中,您需要转换结果,但在 C 中 void*
可以自由转换。
参见 How to tell GCC that a pointer argument is always double-word-aligned? and https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html。
这当然是特定于 GNU C/C++ 方言(clang、gcc、icc)的,不能移植到 MSVC 或其他不支持 GNU 扩展的编译器。
So far I use roughly similar construction for all operations over the array.
多次遍历数组通常比一次遍历尽可能多的循环更糟糕。即使它在 L1D 中仍然很热,与数据在寄存器中时执行更多操作相比,额外的加载和存储指令也是一个瓶颈。
正如 Olaf 指出的那样,可以编写适当的加载和保存函数。所以现在代码很好地转换为加载时的两个 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]));
}
}