我正在运行以下代码:
#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);
有效。
现在我想知道一些事情:
为什么会这样?不应该有任何可能导致访问未分配内存的“内存对齐”问题。如果我的理解有误 - 请让我知道我错过了什么。
(__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/