为什么这个 AVX 内在原因会导致 "Segmentation fault" with clang,而不是 GCC?
Why does this AVX intrinsic cause "Segmentation fault" with clang, but not GCC?
似乎以下两个函数在使用 -mavx(或 -march=sandybridge -> skylake)通过 clang 编译时会导致分段错误。
void _mm256_mul_double_intrin(double* a, double* b, int N)
{
int nb_iters = N / ( sizeof(__m256d) / sizeof(double) );
__m256d* l = (__m256d*)a;
__m256d* r = (__m256d*)b;
for (int i = 0; i < nb_iters; ++i, ++l, ++r)
_mm256_store_pd((double *)l, _mm256_mul_pd(*l, *r));
}
void _mm256_mul_double(double* a, double* b, int N)
{
int nb_iters = N / ( sizeof(__m256d) / sizeof(double) );
__m256d* l = (__m256d*)a;
__m256d* r = (__m256d*)b;
for (int i = 0; i < nb_iters; ++i, ++l, ++r)
__asm__(
"vmulpd %[r], %[l], %[l] \t\n"
: [l] "+x" (*l)
: [r] "m" (*r)
:
);
}
当N为4的2倍或更多(ymm寄存器宽度/双倍宽度)时,clang编译的代码有时会导致段错误。 (见下方的魔杖盒 link)
GCC 编译后的代码似乎没问题。
wandbox.org/permlink/kex4e3lRCKfPAq2J
** 我在 whosebug.com
上找到了原始源代码
可能归结为内存对齐,但是,现代处理器可以 read/write 未对齐内存与未对齐内存一样高效(效率非常接近)所以使用 _mm256_loadu_pd(r)
而不是 *r
和 _mm256_loadu_pd(l)
而不是 *l
和 _mm256_storeu_pd
来存储变量。
答案就在您在 Godbolt 上链接的 asm 中:
gcc 使用 andq $-32, %rsp
将堆栈对齐 32,因此代码中所有需要对齐的加载和存储都不会出错。 (取消引用 __m256d*
和 _mm256_store_pd
而不是 _mm256_storeu_pd
)。 AVX 指令通常不需要对齐,但对齐移动指令(如vmovapd
)需要。
这对于 gcc 仅 可能,因为您的测试用例允许函数使用 double a[]
和 double b[]
上的 __m256d
操作内联到在堆栈上分配数组的函数。
例如:
void ext(double *);
void foo(void) {
double tmp [1024];
ext(tmp);
}
编译为简单分配,不会过度对齐堆栈。
subq 00, %rsp
movq %rsp, %rdi
call ext(double*)
addq 00, %rsp
ret
x86-64 SysV ABI 只需要 16B 堆栈对齐。 (并且 gcc 不会选择维护更多。)因此,如果 ext()
实际上是需要 double*
的 32 字节对齐的函数之一,它就会出错。
gcc 不知道 32B 对齐会提升 ext()
的性能,因此它不会花费指令来对齐所有自动存储数组。如果出现正确性问题,那是你的错!
Clang 甚至在内联之后也不做任何对齐,只是在堆栈上保留 space 和 subq 8, %rsp
。因此,即使在您的测试用例中,堆栈地址-space 随机化也只会在一半时间内为您提供 32B 对齐的堆栈。
如果您使用alignas(32) double a[]
,所有编译器都需要对齐数组。 (alignas
不适用于像 new
或 malloc
这样的动态存储,但它确实适用于自动和静态数组。对于动态,请参阅 )。
似乎以下两个函数在使用 -mavx(或 -march=sandybridge -> skylake)通过 clang 编译时会导致分段错误。
void _mm256_mul_double_intrin(double* a, double* b, int N)
{
int nb_iters = N / ( sizeof(__m256d) / sizeof(double) );
__m256d* l = (__m256d*)a;
__m256d* r = (__m256d*)b;
for (int i = 0; i < nb_iters; ++i, ++l, ++r)
_mm256_store_pd((double *)l, _mm256_mul_pd(*l, *r));
}
void _mm256_mul_double(double* a, double* b, int N)
{
int nb_iters = N / ( sizeof(__m256d) / sizeof(double) );
__m256d* l = (__m256d*)a;
__m256d* r = (__m256d*)b;
for (int i = 0; i < nb_iters; ++i, ++l, ++r)
__asm__(
"vmulpd %[r], %[l], %[l] \t\n"
: [l] "+x" (*l)
: [r] "m" (*r)
:
);
}
当N为4的2倍或更多(ymm寄存器宽度/双倍宽度)时,clang编译的代码有时会导致段错误。 (见下方的魔杖盒 link)
GCC 编译后的代码似乎没问题。
wandbox.org/permlink/kex4e3lRCKfPAq2J
** 我在 whosebug.com
上找到了原始源代码可能归结为内存对齐,但是,现代处理器可以 read/write 未对齐内存与未对齐内存一样高效(效率非常接近)所以使用 _mm256_loadu_pd(r)
而不是 *r
和 _mm256_loadu_pd(l)
而不是 *l
和 _mm256_storeu_pd
来存储变量。
答案就在您在 Godbolt 上链接的 asm 中:
gcc 使用 andq $-32, %rsp
将堆栈对齐 32,因此代码中所有需要对齐的加载和存储都不会出错。 (取消引用 __m256d*
和 _mm256_store_pd
而不是 _mm256_storeu_pd
)。 AVX 指令通常不需要对齐,但对齐移动指令(如vmovapd
)需要。
这对于 gcc 仅 可能,因为您的测试用例允许函数使用 double a[]
和 double b[]
上的 __m256d
操作内联到在堆栈上分配数组的函数。
例如:
void ext(double *);
void foo(void) {
double tmp [1024];
ext(tmp);
}
编译为简单分配,不会过度对齐堆栈。
subq 00, %rsp
movq %rsp, %rdi
call ext(double*)
addq 00, %rsp
ret
x86-64 SysV ABI 只需要 16B 堆栈对齐。 (并且 gcc 不会选择维护更多。)因此,如果 ext()
实际上是需要 double*
的 32 字节对齐的函数之一,它就会出错。
gcc 不知道 32B 对齐会提升 ext()
的性能,因此它不会花费指令来对齐所有自动存储数组。如果出现正确性问题,那是你的错!
Clang 甚至在内联之后也不做任何对齐,只是在堆栈上保留 space 和 subq 8, %rsp
。因此,即使在您的测试用例中,堆栈地址-space 随机化也只会在一半时间内为您提供 32B 对齐的堆栈。
如果您使用alignas(32) double a[]
,所有编译器都需要对齐数组。 (alignas
不适用于像 new
或 malloc
这样的动态存储,但它确实适用于自动和静态数组。对于动态,请参阅