c - SIMD 内在函数 - 段错误

标签 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“Mavericks”的 Mac 上)。

从 a 中删除 +1 并声明 a 时:

__m128 *v_a = (__m128*)(a+1);

有效。

现在我想知道一些事情:

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

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

我正在尝试了解 SIMD 的工作原理,因此您可以链接的任何信息 - 可能会帮助我理解它为什么会这样 react 。

最佳答案

扩展 Cory Nelson 的回答:

每种类型都有一个对齐方式。给定类型的对象“想要”一个地址,该地址是对齐的倍数。例如,float 类型的变量的对齐方式为 4。这从字面意思来说,当您获取 float 的地址并将其转换为整数时,您将得到 4 的倍数,因为编译器永远不会分配一个地址不是 float 的 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)时,这就是您的代码中发生的情况。访问(读取或写入)具有未对齐地址的对象的后果可能是零、性能损失或崩溃,具体取决于处理器架构。

我们可以打印出你的 vector 地址:

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 时,您的代码将不再崩溃。这是因为您对地址“很幸运”: float 必须对齐到 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,因为编译器为每个 float[10] 选择了 16 字节对齐的地址。

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 指令要求的对齐。

关于c - SIMD 内在函数 - 段错误,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/25596379/

相关文章:

c++ - 编译后如何计算某些二进制函数(或基本 block )的校验和?

assembly - 在 TASM 理想模式下设置数据段的对齐方式

c++ - 矢量化 : multiply _m256i elements

c++ - 没有 AVX2 的 32 位整数的 SSE 整数 2^n 次幂

c - 如果我有一个 f = 50,000 的 float ,然后我执行 f*f,返回的值是否为负数?

c - 使用局部静态变量线程安全/可重入的函数

C程序对两个连续数组元素的差进行升序排序

c - 使用 openGL 从 C 中的文件或指针打印值

c++ - 连续迭代器上的 SIMD 指令

assembly - 如何在某处计算正弦值,然后移至汇编中的 XMM0 中?