SIMD内在函数 - 分段错误

eve*_*nro 3 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;
}
Run Code Online (Sandbox Code Playgroud)

并获得分段错误:11(在Mac上运行OS X"Mavericks").

从a中删除+1时,声明如下:

__m128 *v_a = (__m128*)(a+1);
Run Code Online (Sandbox Code Playgroud)

有用.

现在我想知道几件事情:

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

  2. (__m128*)(a + 1)发生了什么转换.

我试图理解SIMD是如何工作的,所以你可以链接的任何信息 - 可能会帮助我理解为什么它会以这种方式做出反应.

rid*_*ish 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);
Run Code Online (Sandbox Code Playgroud)

或者为了更清楚,只是它们的低4位:

printf("%ld %ld %ld\n", (intptr_t)a & 0xF, (intptr_t)b & 0xF,(intptr_t)c & 0xF);
Run Code Online (Sandbox Code Playgroud)

在我的机器上,此输出12 4 12显示地址不是16的倍数,因此不是16字节对齐.(但请注意它们都是4的倍数,因为它们有float-array类型,浮点数必须是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);
Run Code Online (Sandbox Code Playgroud)

现在printf输出0 0 0,因为编译器为每个float选择了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);
Run Code Online (Sandbox Code Playgroud)

这也有效,但便携性较差.

那说,你的+1怎么样:

__m128 *v_a = (__m128*)(a+1);
Run Code Online (Sandbox Code Playgroud)

假设a是对齐的16字节,并且有一个类型float*,则a+1增加sizeof(float)(这是4)到地址,这导致仅仅是4字节对齐地址.这是一个硬件限制,您无法使用正常指令将仅4字节对齐的地址直接加载/存储到SSE寄存器中.它会崩溃!您必须改为使用不同的(较慢的)指令,例如生成的指令_mm_loadu_ps.

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


Cor*_*son 5

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

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

变量保证具有适合它们的类型的对齐方式——在这种情况下,a, b, 并且c将至少对齐 4 字节,因为这是float在您的平台上运行所需的对齐方式。编译器可能有,但是(理所当然地)没有给它们更严格的对齐方式——所以当你强制转换__mm128*和取消引用时,你会得到一个段错误。

与其取消引用指针,不如考虑使用_mm_loadu_psand _mm_storeu_ps,它允许未对齐的访问。或者为了获得更好的性能,请修复对齐方式。