我正在搞乱SSE,尝试编写一个函数,它会将单精度浮点数组的所有值相加。我希望它适用于所有长度的数组,而不仅仅是4的倍数,正如网上几乎所有的例子所假设的那样。我想出了类似的东西:
float sse_sum(const float *x, const size_t n)
{
const size_t
steps = n / 4,
rem = n % 4,
limit = steps * 4;
__m128
v, // vector of current values of x
sum = _mm_setzero_ps(0.0f); // sum accumulator
// perform the main part of the addition
size_t i;
for (i = 0; i < limit; i+=4)
{
v = _mm_load_ps(&x[i]);
sum = _mm_add_ps(sum, v);
}
// add the last 1 - 3 odd items if necessary, based on the remainder value
switch(rem)
{
case 0:
// nothing to do if no elements left
break;
case 1:
// put 1 remaining value into v, initialize remaining 3 to 0.0
v = _mm_load_ss(&x[i]);
sum = _mm_add_ps(sum, v);
break;
case 2:
// set all 4 to zero
v = _mm_setzero_ps();
// load remaining 2 values into lower part of v
v = _mm_loadl_pi(v, (const __m64 *)(&x[i]));
sum = _mm_add_ps(sum, v);
break;
case 3:
// put the last one of the remaining 3 values into v, initialize rest to 0.0
v = _mm_load_ss(&x[i+2]);
// copy lower part containing one 0.0 and the value into the higher part
v = _mm_movelh_ps(v,v);
// load remaining 2 of the 3 values into lower part, overwriting
// old contents
v = _mm_loadl_pi(v, (const __m64*)(&x[i]));
sum = _mm_add_ps(sum, v);
break;
}
// add up partial results
sum = _mm_hadd_ps(sum, sum);
sum = _mm_hadd_ps(sum, sum);
__declspec(align(16)) float ret;
/// and store the final value in a float variable
_mm_store_ss(&ret, sum);
return ret;
}
然后我开始怀疑这是不是有点过分。我的意思是,我陷入SIMD模式,只需要用SSE处理尾部。这很有趣,但是添加尾部并使用常规浮点运算计算结果不是一样好(并且更简单)吗?我是否在SSE中获得了任何收益?
答案 0 :(得分:2)
正如所承诺的那样,我做了一些基准测试。为此,我_aligned_malloc了一个大小为100k的浮点数组,用单个值 1.123f 填充它并测试了相应的函数。我写了一个简单的求和函数,它只是在循环中累积了结果。接下来,我做了一个简化的SSE求和函数变量,其中使用常规浮点数进行水平和尾部添加:
float sseSimpleSum(const float *x, const size_t n)
{
/* ... Everything as before, but instead of hadd: */
// do the horizontal sum on "sum", which is a __m128 by hand
const float *sumf = (const float*)(&sum);
float ret = sumf[0] + sumf[1] + sumf[2] + sumf[3];
// add up the tail
for (; i < n; ++i)
{
ret += x[i];
}
return ret;
}
我没有受到性能影响,有时甚至看起来稍微快一些,但我发现计时器非常不可靠,所以让我们假设简化的变体等同于复杂的变体。令人惊讶的是,从SSE和天真浮点求和函数获得的值的差异相当大。我怀疑这是由于四舍五入的错误累积,所以我写了一个基于Kahan algorithm的函数,它给出了正确的结果,虽然比天真浮点加法慢。为了完整起见,我在这些方面做了一个基于SSE Kahan的功能:
float SubsetTest::sseKahanSum(const float *x, const size_t n)
{
/* ... init as before... */
__m128
sum = _mm_setzero_ps(), // sum accumulator
c = _mm_setzero_ps(), // correction accumulator
y, t;
// perform the main part of the addition
size_t i;
for (i = 0; i < limit; i+=4)
{
y = _mm_sub_ps(_mm_load_ps(&x[i]), c);
t = _mm_add_ps(sum, y);
c = _mm_sub_ps(_mm_sub_ps(t, sum), y);
sum = t;
}
/* ... horizontal and tail sum as before... */
}
以下是从发布模式下的VC ++ 2010获得的基准测试结果,显示了获得的总和值,计算所需的时间以及与正确值相关的误差量:
Kahan:值= 112300,时间= 1155,错误= 0
浮点数:值= 112328.78125,时间= 323,错误= 28.78125
SSE:值= 112304.476563,时间= 46,错误= 4.4765625
简单SSE:值= 112304.476563,时间= 45,错误= 4.4765625
Kahan SSE:值= 112300,时间= 167,错误= 0
天真浮动添加的错误数量巨大!我怀疑非Kahan SSE函数更准确,因为它们相当于Pairwise summation,这可以比直接方法产生准确性改进。 Kahan SSE是准确的,但只是天然浮子添加速度的两倍。
答案 1 :(得分:2)
我会查看Agner Fog的矢量类。请参阅“当数据大小不是VectorClass.pdf的矢量大小的倍数”时。他列举了五种不同的方法,并讨论了每种方法的优缺点。 http://www.agner.org/optimize/#vectorclass
一般来说,我这样做的方式来自以下链接。 http://fastcpp.blogspot.no/2011/04/how-to-unroll-loop-in-c.html
#define ROUND_DOWN(x, s) ((x) & ~((s)-1))
void add(float* result, const float* a, const float* b, int N) {
int i = 0;
for(; i < ROUND_DOWN(N, 4); i+=4) {
__m128 a4 = _mm_loadu_ps(a + i);
__m128 b4 = _mm_loadu_ps(b + i);
__m128 sum = _mm_add_ps(a4, b4);
_mm_storeu_ps(result + i, sum);
}
for(; i < N; i++) {
result[i] = a[i] + b[i];
}
}
答案 2 :(得分:1)
在这种情况下,除非你能指出一些真正的性能提升,否则它可能会过度杀伤。如果您使用的是gcc
,那么Auto-vectorization with gcc 4.7上的这个指南可能是一个不错的选择,虽然它显然会gcc
具体,但它并不像内在函数那样难看。