3

単精度浮動小数点配列のすべての値を合計する関数を作成しようとして、SSEをいじっていました。Web上のほぼすべての例で想定されているように、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でそれを行うことで何かを得ていますか?

4

3 に答える 3

2

約束通り、私はいくつかのベンチマークテストを行いました。この目的のために、サイズ100kのfloat配列を_aligned_mallocし、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とナイーブフロート合計関数から得られた値のかなり大きな違いでした。これは丸めによるエラーの蓄積によるものと思われたので、単純な浮動小数点の加算よりもはるかに遅いものの、正しい結果が得られるカハンアルゴリズムに基づいた関数を作成しました。完全を期すために、私はこれらの線に沿ってSSEKahanベースの関数を作成しました。

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から取得されたベンチマーク結果であり、合計の取得値、計算にかかった時間、および正しい値に関連するエラーの量を示しています。

カハン:値= 112300、時間= 1155、エラー= 0
フロート:値= 112328.78125、時間= 323、エラー= 28.78125
SSE:値= 112304.476563、時間= 46、エラー= 4.4765625
単純なSSE:値= 112304.476563、時間= 45、エラー=4.4765625Kahan
SSE:値= 112300、時間= 167、エラー= 0

素朴なフロート加算のエラー量は膨大です!非KahanSSE関数は、ペアワイズ合計になり、単純なアプローチよりも精度が向上する可能性があるため、より正確であると思います。Kahan SSEは正確ですが、単純なフロートの追加の約2倍の速度しかありません。

于 2013-03-20T02:20:17.877 に答える
2

AgnerFogのvectorclassをチェックします。VectorClass.pdfの「データサイズがベクトルサイズの倍数でない場合」のセクションを参照してください。彼はこれを行うための5つの異なる方法をリストし、それぞれの長所と短所について説明します。 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];
  }
}
于 2013-03-20T15:29:01.590 に答える
1

この場合、実際のパフォーマンスの向上を指摘できない限り、やり過ぎかもしれません。使用している場合は、gcc 4.7を使用した自動ベクトル化gccに関するこのガイドが優れた代替手段になる可能性がありますが、組み込み関数ほど醜いものではないことは明らかです。gcc

于 2013-03-20T00:19:15.397 に答える