SIMD内在函数 - 分段错误

时间:2014-08-31 21:18:34

标签: c x86 sse simd

我正在运行以下代码:

#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);

有效。

现在我想知道几件事:

  1. 为什么会这样?应该没有任何“内存对齐”问题可能导致访问未分配的内存。如果我的理解错了 - 请让我知道我错过了什么。

  2. 使用(__m128 *)(a + 1)进行了什么转换。

  3. 我想了解SIMD是如何工作的,所以你可以链接的任何信息 - 可以帮助我理解它为什么会这样做。

2 个答案:

答案 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+1sizeof(float)(即4)添加到地址,这会产生一个地址只是4字节对齐。这是一个硬件限制,您无法使用正常指令将仅4字节对齐的地址直接加载/存储到SSE寄存器中。它会崩溃!您必须改为使用不同(较慢)的指令,例如_mm_loadu_ps生成的指令。

确保正确对齐是使用SIMD指令集的挑战之一。您经常会看到SIMD算法使用“普通”(标量)代码处理前几个元素,以便它可以达到SIMD指令所需的对齐。

答案 1 :(得分:4)

对齐不是可用空间的函数,而是该空间在内存中的位置。当人们谈话时,这意味着地址必须是可分割的。

SSE要求加载/存储地址为16字节对齐。例如。您希望使用的地址为01632等,但不是42036

保证变量的类型具有适当的对齐方式 - 在这种情况下,abc将至少对齐4个字节,因为这是对齐方式float需要在您的平台上运行。编译器可以拥有,但是(理所当然地)没有给它们更严格的对齐 - 所以当你转向__mm128*并取消引用时,你就会遇到段错误。

不要取消引用指针,而应考虑使用_mm_loadu_ps_mm_storeu_ps,它们允许未对齐访问。或者为了获得更好的性能,请修复对齐。