我正在运行以下代码:
#include <emmintrin.h>
#include <stdlib.h>
#include <stdio.h>
int main(int argv, char** argc)
{
float a[] = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0};
float b[] = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0};
float c[] = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0};
__m128 *v_a = (__m128*)(a+1); // Trying to create c[i] = a[i=1] * b[i];
__m128 *v_b = (__m128*)(b);
__m128 *v_c = (__m128*)(c);
for (int i=0; i < 1; i++)
{
*v_c = _mm_mul_ps(*v_a,*v_b);
v_a++;
v_b++;
v_c++;
}
for (int i=0; i<= 9;i++)
{
printf("%f\n",c[i]);
}
return 0;
}
并且出现分段错误:11(在运行OS X的Mac上“Mavericks”)。
从a中删除+1时,声明如下:
__m128 *v_a = (__m128*)(a+1);
有效。
现在我想知道几件事:
为什么会这样?应该没有任何“内存对齐”问题可能导致访问未分配的内存。如果我的理解错了 - 请让我知道我错过了什么。
使用(__m128 *)(a + 1)进行了什么转换。
我想了解SIMD是如何工作的,所以你可以链接的任何信息 - 可以帮助我理解它为什么会这样做。
答案 0 :(得分:6)
扩展Cory Nelson的答案:
每种类型都有一个对齐方式。给定类型的对象“想要”地址是对齐的倍数。例如,float类型的变量具有4的对齐。这意味着,当你获取float的地址并将其转换为整数时,你将获得4的倍数,因为编译器永远不会分配地址不是4的浮点数的倍数。
在32位x86上,这里是一些示例对齐:char = 1,short = 2,int = 4,long long = 4,float = 4,double = 4,void * = 4,SSE vector = 16。对齐总是2的幂。
如果我们将指针指向具有更严格(更大)对齐的不同指针类型,我们可能会得到一个未对齐的地址。当您将float *
(对齐方式4)转换为__m128 *
(对齐方式16)时,代码中会发生这种情况。访问(读取或写入)具有未对齐地址的对象的后果可能无关紧要,性能损失或崩溃,具体取决于处理器体系结构。
我们可以打印出你的载体地址:
printf("%p %p %p\n", a, b, c);
或者为了更清楚,只是它们的低4位:
printf("%ld %ld %ld\n", (intptr_t)a & 0xF, (intptr_t)b & 0xF,(intptr_t)c & 0xF);
在我的机器上,输出12 4 12
,表明地址不是16的倍数,因此不是16字节对齐的。 (但请注意它们都是4的倍数,因为它们的类型为float-float,浮点数必须是4字节对齐。)
删除+1后,您的代码不再崩溃。这是因为你对地址“变得幸运”:浮点数必须与4的倍数对齐,但它们恰好也与16的倍数对齐。这是一颗定时炸弹!在你的代码中调整一些东西(比如说,引入另一个变量),或者改变优化级别,它可能会开始崩溃!您需要明确地对齐变量。
那么如何对齐它们呢?声明变量时,编译器(不是你)在内存中选择一个存在该变量的地址。它试图尽可能地将变量打包在一起,以避免浪费空间,但仍然必须确保地址的类型具有正确的对齐方式。
增加对齐的最佳方法之一是使用一个union,它包含一个类型,它的对齐方式就是你需要的:
union vec {
float f[10];
__m128 v;
};
union vec av = {.f = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0}};
union vec bv = {.f = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0}};
union vec cv = {.f = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0}};
float *a = av.f;
float *b = bv.f;
float *c = cv.f;
printf("%ld %ld %ld\n", (intptr_t)a & 0xF, (intptr_t)b & 0xF,(intptr_t)c & 0xF);
现在printf输出0 0 0
,因为编译器为每个浮点数选择了16字节对齐的地址[10]。
gcc和clang还允许您直接请求对齐:
float a[] __attribute__ ((aligned (16))) = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0};
float b[] __attribute__ ((aligned (16))) = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0};
float c[] __attribute__ ((aligned (16))) = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0};
printf("%ld %ld %ld\n", (intptr_t)a & 0xF, (intptr_t)b & 0xF,(intptr_t)c & 0xF);
这也有效,但便携性较差。
那说,你的+1:
__m128 *v_a = (__m128*)(a+1);
假设a
是16字节对齐且类型为float*
,则a+1
将sizeof(float)
(即4)添加到地址,这会产生一个地址只是4字节对齐。这是一个硬件限制,您无法使用正常指令将仅4字节对齐的地址直接加载/存储到SSE寄存器中。它会崩溃!您必须改为使用不同(较慢)的指令,例如_mm_loadu_ps
生成的指令。
确保正确对齐是使用SIMD指令集的挑战之一。您经常会看到SIMD算法使用“普通”(标量)代码处理前几个元素,以便它可以达到SIMD指令所需的对齐。
答案 1 :(得分:4)
对齐不是可用空间的函数,而是该空间在内存中的位置。当人们谈话时,这意味着地址必须是可分割的。
SSE要求加载/存储地址为16字节对齐。例如。您希望使用的地址为0
,16
,32
等,但不是4
,20
或36
。
保证变量的类型具有适当的对齐方式 - 在这种情况下,a
,b
和c
将至少对齐4个字节,因为这是对齐方式float
需要在您的平台上运行。编译器可以拥有,但是(理所当然地)没有给它们更严格的对齐 - 所以当你转向__mm128*
并取消引用时,你就会遇到段错误。
不要取消引用指针,而应考虑使用_mm_loadu_ps
和_mm_storeu_ps
,它们允许未对齐访问。或者为了获得更好的性能,请修复对齐。