SSE SIMD 代码中的性能问题
Performance issue in SSE SIMD code
我有一个代码可以将一个向量围绕另一个向量旋转到给定的角度。我使用四元数和这个 fast formula 来做到这一点。我写了两个变体,使用和不使用 SIMD 编译器内部函数。
变体 1:
#include <xmmintrin.h>
#include <pmmintrin.h>
#include "test2.h"
static __v4sf cross_product_ (__v4sf a, __v4sf b)
{
__v4sf r1 = a * _mm_shuffle_ps (b, b, _MM_SHUFFLE (1, 3, 2, 0));
__v4sf r2 = b * _mm_shuffle_ps (a, a, _MM_SHUFFLE (1, 3, 2, 0));
__v4sf r = r1 - r2;
return _mm_shuffle_ps (r, r, _MM_SHUFFLE (1, 3, 2, 0));
}
static __v4sf rotate_vector_ (__v4sf base, __v4sf vect)
{
__v4sf base_re = _mm_shuffle_ps (base, base, 0);
__v4sf tmp = cross_product_ (base, vect);
tmp = tmp * _mm_set_ps1 (2.0);
__v4sf res = vect + base_re*tmp + cross_product_ (base, tmp);
return res;
}
void rotate_vector (float base[], float vect[], float res[])
{
__v4sf v = _mm_slli_si128 (_mm_load_ps (vect), 4);
__v4sf r = rotate_vector_ (_mm_load_ps (base), v);
r = _mm_srli_si128 (r, 4);
_mm_store_ps (res, r);
}
变体 2:
#include "test2.h"
static void cross_product (const float v1[], const float v2[], float res[])
{
res[0] = v1[1]*v2[2] - v1[2]*v2[1];
res[1] = -v1[0]*v2[2] + v1[2]*v2[0];
res[2] = v1[0]*v2[1] - v1[1]*v2[0];
}
void rotate_vector (float base[], float vector[], float res[])
{
float tmp[3], tmp2[3];
int i;
cross_product (base+1, vector, tmp);
for (i=0; i<3; i++) tmp[i] *= 2.0;
cross_product (base+1, tmp, tmp2);
for (i=0; i<3; i++) res[i] = vector[i] + base[0]*tmp[i] + tmp2[i];
}
四元数的数据布局:
0......32......64......96......128 bits
1(real) i j k
对于矢量:
0......32......64......96......128 bits
x y z XXX
然后我尝试用一个旋转四元数(绕轴 x 旋转 90 度)来旋转一个预初始化的向量数组。使用大量 RAM!
#include <sys/time.h>
#include <stdlib.h>
#include <stdio.h>
#include <math.h>
#include <strings.h>
#include "test2.h"
double gettime ()
{
struct timeval tv;
gettimeofday (&tv, NULL);
return (double)tv.tv_sec + (0.000001 * (double)tv.tv_usec);
}
#define N 400000000
int main ()
{
float z = sqrtf(2)/2;
float a[4] __attribute__((aligned(16))) = {z,z,0,0};
float (*b)[4] = aligned_alloc (16, 4*N*sizeof(float));
int i;
for (i=0; i<N; i++)
{
bzero (b[i], 16);
b[i][i%3] = 4;
b[i][0] = 1;
}
double time = gettime();
for (i=0; i<N; i++)
{
#if 0
b[i][0] = 1;
#endif
rotate_vector (a,b[i],b[i]);
}
time = gettime() - time;
printf ("%f %f %f\n", b[0][0], b[0][1], b[0][2]);
printf ("%f\n", time);
return 0;
}
当使用 clang 3.4 -O3 -msse3
编译并在 AMD FX-6300 处理器上执行时,SIMD 变体比非 SIMD 快大约 10%。但是,如果我删除 #if/#endif
,换句话说,将某些内容写入必须在每次迭代中旋转的向量,SIMD 变体会慢很多并且执行速度比非 SIMD 慢 2-2.5。那么单次写入如何减慢整个过程呢?它与缓存有关吗?我正在使用 FreeBSD 10.2 并尝试使用 pmcstat(8) 测试此代码,但没有发现任何异常情况(例如高缓存未命中率或类似情况)。
不过,Atom 处理器的性能似乎没有受到影响(在 Asus Zenfone 2 ze551ml 智能手机和 Acer Aspire One Happy 2 上网本上测试)。所以也许这是特定于处理器的问题?或者我对 SIMD 的理解不正确,这不是应用它们的正确位置?
如果你想在你的机器上编译这个例子,这里缺少 test2.h(你需要 ~6Gb 的 RAM):
#ifndef TEST2_H
#define TEST2_H
void quat_mul (float a[], float b[], float c[]);
void rotate_vector (float base[], float vect[], float res[]);
#endif
在执行向量加载之前立即写入单个元素将导致存储转发停顿。这可能会损害您的 SIMD 版本的性能。您可以使用可以记录性能计数器的分析工具进行检查。请参阅 Agner Fog's guides, and other links from the x86 标签 wiki。
哦,我刚刚注意到你说 Atom 上的性能不受影响。这是支持我的理论的有力证据:Atom 具有惊人的存储转发能力,并且可以将数据从一个狭窄的存储转发到它后面的一个广泛的负载。在所有其他 x86 微体系结构上,这会导致存储转发停顿并具有更高的延迟。 Agner Fog 的 microarch pdf 对此进行了解释。
如果要修改单个向量元素,最好使用 _mm_insert_ps
来完成。如果你想修改很多,那很有可能。最好使用 _mm_set_ps
来制作一个新的矢量,然后 _mm_blend_ps
与旧矢量结合。 _mm_shuffle_ps
和_mm_unpacklo_ps
/_mm_unpackhi_ps
(或pd
)也可以组合向量之间的数据。
您有足够的数据 (6.4 GB),缓存根本不重要。
在每次迭代中,您修改内存中的一个向量元素,用 load_ps 加载向量,进行一些计算,然后将其写回。所以有一个非矢量写入,然后是一个矢量写入。第一次写入将强制加载缓存行,然后将其部分弄脏,然后将其作为向量读取并作为向量写入。这一切都很复杂,并且取决于处理器和内存系统的精确设计可能会导致速度减慢。
如果实际使用了 b [i] [0],我很可能会将赋值 b [i] [0] = 1 移动到您调用的函数中。所以在向量版本中,你 load_ps 向量 b [i],然后在向量寄存器 中修改向量 的第一个元素,避免干扰内存。
我有一个代码可以将一个向量围绕另一个向量旋转到给定的角度。我使用四元数和这个 fast formula 来做到这一点。我写了两个变体,使用和不使用 SIMD 编译器内部函数。
变体 1:
#include <xmmintrin.h>
#include <pmmintrin.h>
#include "test2.h"
static __v4sf cross_product_ (__v4sf a, __v4sf b)
{
__v4sf r1 = a * _mm_shuffle_ps (b, b, _MM_SHUFFLE (1, 3, 2, 0));
__v4sf r2 = b * _mm_shuffle_ps (a, a, _MM_SHUFFLE (1, 3, 2, 0));
__v4sf r = r1 - r2;
return _mm_shuffle_ps (r, r, _MM_SHUFFLE (1, 3, 2, 0));
}
static __v4sf rotate_vector_ (__v4sf base, __v4sf vect)
{
__v4sf base_re = _mm_shuffle_ps (base, base, 0);
__v4sf tmp = cross_product_ (base, vect);
tmp = tmp * _mm_set_ps1 (2.0);
__v4sf res = vect + base_re*tmp + cross_product_ (base, tmp);
return res;
}
void rotate_vector (float base[], float vect[], float res[])
{
__v4sf v = _mm_slli_si128 (_mm_load_ps (vect), 4);
__v4sf r = rotate_vector_ (_mm_load_ps (base), v);
r = _mm_srli_si128 (r, 4);
_mm_store_ps (res, r);
}
变体 2:
#include "test2.h"
static void cross_product (const float v1[], const float v2[], float res[])
{
res[0] = v1[1]*v2[2] - v1[2]*v2[1];
res[1] = -v1[0]*v2[2] + v1[2]*v2[0];
res[2] = v1[0]*v2[1] - v1[1]*v2[0];
}
void rotate_vector (float base[], float vector[], float res[])
{
float tmp[3], tmp2[3];
int i;
cross_product (base+1, vector, tmp);
for (i=0; i<3; i++) tmp[i] *= 2.0;
cross_product (base+1, tmp, tmp2);
for (i=0; i<3; i++) res[i] = vector[i] + base[0]*tmp[i] + tmp2[i];
}
四元数的数据布局:
0......32......64......96......128 bits
1(real) i j k
对于矢量:
0......32......64......96......128 bits
x y z XXX
然后我尝试用一个旋转四元数(绕轴 x 旋转 90 度)来旋转一个预初始化的向量数组。使用大量 RAM!
#include <sys/time.h>
#include <stdlib.h>
#include <stdio.h>
#include <math.h>
#include <strings.h>
#include "test2.h"
double gettime ()
{
struct timeval tv;
gettimeofday (&tv, NULL);
return (double)tv.tv_sec + (0.000001 * (double)tv.tv_usec);
}
#define N 400000000
int main ()
{
float z = sqrtf(2)/2;
float a[4] __attribute__((aligned(16))) = {z,z,0,0};
float (*b)[4] = aligned_alloc (16, 4*N*sizeof(float));
int i;
for (i=0; i<N; i++)
{
bzero (b[i], 16);
b[i][i%3] = 4;
b[i][0] = 1;
}
double time = gettime();
for (i=0; i<N; i++)
{
#if 0
b[i][0] = 1;
#endif
rotate_vector (a,b[i],b[i]);
}
time = gettime() - time;
printf ("%f %f %f\n", b[0][0], b[0][1], b[0][2]);
printf ("%f\n", time);
return 0;
}
当使用 clang 3.4 -O3 -msse3
编译并在 AMD FX-6300 处理器上执行时,SIMD 变体比非 SIMD 快大约 10%。但是,如果我删除 #if/#endif
,换句话说,将某些内容写入必须在每次迭代中旋转的向量,SIMD 变体会慢很多并且执行速度比非 SIMD 慢 2-2.5。那么单次写入如何减慢整个过程呢?它与缓存有关吗?我正在使用 FreeBSD 10.2 并尝试使用 pmcstat(8) 测试此代码,但没有发现任何异常情况(例如高缓存未命中率或类似情况)。
不过,Atom 处理器的性能似乎没有受到影响(在 Asus Zenfone 2 ze551ml 智能手机和 Acer Aspire One Happy 2 上网本上测试)。所以也许这是特定于处理器的问题?或者我对 SIMD 的理解不正确,这不是应用它们的正确位置?
如果你想在你的机器上编译这个例子,这里缺少 test2.h(你需要 ~6Gb 的 RAM):
#ifndef TEST2_H
#define TEST2_H
void quat_mul (float a[], float b[], float c[]);
void rotate_vector (float base[], float vect[], float res[]);
#endif
在执行向量加载之前立即写入单个元素将导致存储转发停顿。这可能会损害您的 SIMD 版本的性能。您可以使用可以记录性能计数器的分析工具进行检查。请参阅 Agner Fog's guides, and other links from the x86 标签 wiki。
哦,我刚刚注意到你说 Atom 上的性能不受影响。这是支持我的理论的有力证据:Atom 具有惊人的存储转发能力,并且可以将数据从一个狭窄的存储转发到它后面的一个广泛的负载。在所有其他 x86 微体系结构上,这会导致存储转发停顿并具有更高的延迟。 Agner Fog 的 microarch pdf 对此进行了解释。
如果要修改单个向量元素,最好使用 _mm_insert_ps
来完成。如果你想修改很多,那很有可能。最好使用 _mm_set_ps
来制作一个新的矢量,然后 _mm_blend_ps
与旧矢量结合。 _mm_shuffle_ps
和_mm_unpacklo_ps
/_mm_unpackhi_ps
(或pd
)也可以组合向量之间的数据。
您有足够的数据 (6.4 GB),缓存根本不重要。
在每次迭代中,您修改内存中的一个向量元素,用 load_ps 加载向量,进行一些计算,然后将其写回。所以有一个非矢量写入,然后是一个矢量写入。第一次写入将强制加载缓存行,然后将其部分弄脏,然后将其作为向量读取并作为向量写入。这一切都很复杂,并且取决于处理器和内存系统的精确设计可能会导致速度减慢。
如果实际使用了 b [i] [0],我很可能会将赋值 b [i] [0] = 1 移动到您调用的函数中。所以在向量版本中,你 load_ps 向量 b [i],然后在向量寄存器 中修改向量 的第一个元素,避免干扰内存。