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 标签 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],然后在向量寄存器 中修改向量 的第一个元素,避免干扰内存。