2016-07-27 10 views
1

popcntとSSE4.2を使用して、CPUのアレイの近似平方根をより速く計算するにはどうすればよいですか?アレイの近似平方根近似

入力は、浮動小数点配列に格納された正の整数(0〜約200,000の範囲)です。

出力は浮動小数点数の配列です。

どちらのアレイも、sseの正しいメモリ配置を持っています。

コードのみ1つのXMMレジスタを使用して下には、Linux上で動作し、ありがとうgcc -O3 code.cpp -lrt -msse4.2

でコンパイルすることができます。

#include <iostream> 
#include <emmintrin.h> 
#include <time.h> 

using namespace std; 
void print_xmm(__m128 xmm){ 
    float out[4]; 
    _mm_storeu_ps(out,xmm); 
    int i; 
    for (i = 0; i < 4; ++i) std::cout << out[i] << " "; 
    std::cout << std::endl; 
} 

void print_arr(float* ptr, size_t size){ 
    size_t i; 
    for(i = 0; i < size; ++i){ 
     cout << ptr[i] << " "; 
    } 
    cout << endl; 
} 

int main(void){ 
    size_t size = 25000 * 4; 
     // this has to be multiple of 4 
    size_t repeat = 10000; 
     // test 10000 cycles of the code 
    float* ar_in = (float*)aligned_alloc(16, size*sizeof(float)); 
    float* ar_out = (float*)aligned_alloc(16, size*sizeof(float)); 
    //fill test data into the input array 
    //the data is an array of positive numbers. 
    size_t i; 
    for (i = 0; i < size; ++i){ 
     ar_in[i] = (i+1) * (i+1); 
    } 
    //prepare for recipical square root. 
    __m128 xmm0; 
    size_t size_fix = size*sizeof(float)/sizeof(__m128); 
    float* ar_in_end = ar_in + size_fix; 
    float* ar_out_now; 
    float* ar_in_now; 
    //timing 
    struct timespec tp_start, tp_end; 
    i = repeat; 
    clock_gettime(CLOCK_MONOTONIC, &tp_start); 
    //start timing 
    while(--i){ 
     ar_out_now = ar_out; 
     for(ar_in_now = ar_in; 
      ar_in_now != ar_in_end; 
      ar_in_now += 4, ar_out_now+=4){ 
      //4 = sizeof(__m128)/sizeof(float); 
      xmm0 = _mm_load_ps(ar_in_now); 
      //cout << "load xmm: "; 
      //print_xmm(xmm0); 
      xmm0 = _mm_rsqrt_ps(xmm0); 
      //cout << "rsqrt xmm: "; 
      //print_xmm(xmm0); 
      _mm_store_ps(ar_out_now,xmm0); 
     } 
    } 
    //end timing 
    clock_gettime(CLOCK_MONOTONIC, &tp_end); 
    double timing; 
    const double nano = 0.000000001; 

    timing = ((double)(tp_end.tv_sec - tp_start.tv_sec) 
      + (tp_end.tv_nsec - tp_start.tv_nsec) * nano)/repeat; 

    cout << " timing per cycle: " << timing << endl; 
    /* 
    cout << "input array: "; 
    print_arr(ar_in, size); 
    cout << "output array: "; 
    print_arr(ar_out,size); 
    */ 
    //free mem 
    free(ar_in); 
    free(ar_out); 
    return 0; 
} 
+3

skylakeのようなものでは、 'rsqrtps'は4サイクルの待ち時間を持ちますが、パイプライン化されているので、毎回新しい' rsqrtps'が発行されます。おそらく、ループを最大4回展開することでスピードアップを得ることができますが、結果はすぐに処理されるのではなく保存されるため、レジスタの名前変更とアウトオブオーダーの実行はおそらく、今ある。 – EOF

+0

このコードの並列版を書くと、誤った共有以外のことについて考える必要がありますか? – rxu

+0

データ競合を避けるようにする必要があります。たとえば、配列を分割する場合は、重複がないことを確認します。同じ結果を同じ場所に2回書くのはOKだと思いますが、配列をアトミックとして宣言しない限り、そうではありません。 – EOF

答えて

4

浮動小数点配列の大きさはどれくらいですか?それが既にL1(またはおそらくL2)で暑いなら、gcc5。3回の出力は、最新のIntel CPUでuopスループットのボトルネックを解消します。これは、反復ごとに1つのベクトルを実行する6つの融合ドメインuopでループを作成するためです。 (したがって、2サイクルごとに1つのベクトルで実行されます)。

現代のIntel CPUで1つのベクトル・スループットを達成するには、ループをアンロールする必要があります。(アンロールされていないasmが動作しない理由は以下を参照してください)。コンパイラがあなたのためにそれを行うことは、おそらく(C++ソースで手作業で行うのではなく)良いでしょう。例えばプロファイルに基づく最適化(gcc -fprofile-use)を使用するか、盲目的に-funroll-loopsを使用してください。


クロックあたり16バイトで理論上、1つのコアでメインメモリの帯域幅を飽和させるのに十分です。しかし、IIRC Z Bosonは複数のコアを使用した方が、より多くの帯域幅が要求されています。入力がコアのL2で暑い場合は、おそらくそのコアを使用してデータを処理するのが最善です。

Haswell以降では、1クロックあたり16バイトがロードされ、格納されるため、L1キャッシュ帯域幅の半分にすぎません。このため、コアあたりの最大帯域幅までのAVXバージョンが必要です。

大きな配列に複数のスレッドを使用している場合は、メモリにボトルネックがある場合はyou might want to do a Newton-Raphson iteration to get a nearly-full accuracy 1/sqrt(x)です。(1つのスレッドが1つの負荷+クロックを1つのクロックで維持することができない場合は、これで問題ありません)

このデータを後でロードするときは、ただちにrsqrtを使用してください。 非常にですが、高スループットですが、FPと同様の遅延があります。また、キャッシュに収まらない大規模な配列の場合、データに対するより少ない分離パスを実行することによって計算の強度が向上することは大きな問題です。

可能な場合には、キャッシュバイパスNTストアを最後の手段として使用してください(Cache Blocking aka Loop Tilingも可能ですが、これを行うことも可能です)。キャッシュを有効に活用する方法を見つけることはできません。使用しようとしているデータを変換することができれば、はるかに優れているので、次に使用するときにキャッシュに格納されます。 (.L31 to jne .L31 on the Godbolt compiler explorerから)


メインループは、6つのインテルSNBファミリーCPU用のuop、indexed addressing modes don't micro-fuseからです。 (残念ながらAgner Fog's microarch pdfにはまだ書かれていません)

Nehalemは4つのALU uopsを持つ4つの融合ドメインuopsであるため、Nehalemは1クロックで1を実行します。

.L31: # the main loop: 6 uops on SnB-family, 4 uops on Nehalem 
    rsqrtps xmm0, XMMWORD PTR [rbx+rax]  # tmp127, MEM[base: ar_in_now_10, index: ivtmp.51_61, offset: 0B] 
    movaps XMMWORD PTR [rbp+0+rax], xmm0  # MEM[base: ar_out_now_12, index: ivtmp.51_61, offset: 0B], tmp127 
    add  rax, 16 # ivtmp.51, 
    cmp  rax, 100000  # ivtmp.51, 
    jne  .L31  #, 

あなたが別の目的地を書きたいので、それはアンロールすることなく、クロックあたり1つのベクトルで実行できるようにダウン4融合ドメインのuopへのループを取得する方法はありません。 (ロードとストアの両方が1レジスタのアドレッシングモードである必要があるため、を使用してをインクリメントする代わりにcurrent_dstというインデックスを使用するというトリックは機能しません)。

gccがポインタインクリメントを使用するようにC++を修正すると、srcとdstをインクリメントする必要があるため、1つのuopしか保存されません。すなわち、float *endp = start + length;for (p = start ; p < endp ; p+=4) {}は、彼らはまだインデックス化アドレッシングモードを使用している場合はそうでないrsqrtps + movapsが自分で4融合ドメインのuopになりますアンロールしながら、うまくいけばのgccはこのような何かを行います

.loop: 
    rsqrtps xmm0, [rsi] 
    add  rsi, 16 
    movaps [rdi], xmm0 
    add  rdi, 16 
    cmp  rdi, rbx 
    jne  .loop 

のようなループになるだろう、アンローリング量がないと、ループごとに1つのベクトルでループが実行されません。

+1

興味深いことに、Agner Fogの命令表によれば、Intel Atomのスループットにはスカラーrsqrtssを使用する方が良いでしょう。これは本当なら面白いです。 – EOF

1

あなたはもちろん、それを測定する必要がありますが、逆平方根を(非常に正確ではない)を計算するために知られているコードがある、(VS2015およびGCC 5.4.0でテストhttps://www.beyond3d.com/content/articles/8/

float InvSqrt (float x) { 
    float xhalf = 0.5f*x; 
    int i = *(int*)&x; 
    i = 0x5f3759df - (i>>1); 
    x = *(float*)&i; 
    x = x*(1.5f - xhalf*x*x); 
    return x; 
} 

をチェックSSE2に)変換、link

__m128 InvSqrtSSE2(__m128 x) { 
    __m128 xhalf = _mm_mul_ps(x, _mm_set1_ps(0.5f)); 

    x = _mm_castsi128_ps(_mm_sub_epi32(_mm_set1_epi32(0x5f3759df), _mm_srai_epi32(_mm_castps_si128(x), 1))); 

    return _mm_mul_ps(x, _mm_sub_ps(_mm_set1_ps(1.5f), _mm_mul_ps(xhalf, _mm_mul_ps(x, x)))); 
} 

UPDATE I

Mえっ!不適切な変換に気づいた@EOFのおかげで、それらはキャストに置き換えられます

+4

'rsqrtps'より速い方法はありません.2つの乗算だけではすでに遅いです。 – harold

+0

@harold私はそれも疑いますが、測定に興味があります。 –

+1

あなたが気付いたのは未定義の動作ですCでは、b)未定義の動作のポイントは、値を変換するあなたの提案されたコードとはちょうど対照的に、 'float'引数のビット表現を' int'として解釈しようとしていることです。 – EOF

3

これは算術強度が非常に低いストリーミング計算であるため、ほぼ確実にメモリにバインドされています。非一時的なロードとストアを使用すると、測定可能なスピードアップが見つかる可能性が高くなります。

// Hint to the CPU that we don't want to use the cache 
// Be sure to update this if you use manual loop unrolling 
_mm_prefetch(reinterpret_cast<char*>(ar_in+4), _MM_HINT_NTA); 

// Hint to the CPU that we don't need to write back through the cache 
_mm_stream_ps(ar_out_now,xmm0); 

EDIT

私は物事が異なるハードウェア上でどのように見えるかを確認するためにいくつかのテストを実行しました。意外なことに、結果は使用中のハードウェアに非常に敏感です。現代のCPUの読み書きバッファ数が増えたためと思われます。

すべてのコードは、2.5GHz帯@

gcc -std=c++14 -O3 -march=native -mavx -mfpmath=sse -ffast-math 

インテルCore i3-3120Mでのgcc-6.1を使用してコンパイルされました。 3MBキャッシュ

OP's code:    350 +- 10 milliseconds 
NTA Prefetch:   360 +- 5 milliseconds 
NTA Prefetch+NTA store: 430 +- 10 milliseconds 

Intel Core i7-6500U CPU @ 2.50GHz; 4MBのキャッシュ2メガバイトに問題のサイズを大きくする

OP's code:    205 +- 5 milliseconds 
NTA Prefetch:   220 +- 2 milliseconds 
NTA Prefetch+NTA store: 200 +- 5 milliseconds 

は、NTAプリフェッチ+ NTAストアはOPのソリューション上で実行中〜30%の減少を見ています。

結果:問題のサイズが小さすぎてNTAから実質的に利益を得ることができません。古いアーキテクチャでは、それは有害です。より新しいアーキテクチャでは、OPのソリューションと同等です。

結論:おそらくこの場合は余計な努力をする価値はないでしょう。

+1

Mmm ...線形RAMアクセスパターンの手動プリフェッチは意味がありませんが、もう一度測定値を見てみたいと思います。 –

+0

プリフェッチの動作とヒントの詳細データをキャッシュから保持するためにCPUに送信します。ヒントが使用されている場合、CPUは追い出しを行いません。 – Tim

+2

'prefetchNTA'は実際のハードウェア上のWBメモリに本当に差をつけますか?私はそれをテストしていませんが、私の推測では、セットアソシエイティブキャッシュのMRU位置ではなく、LRU位置に新しくロードされた行を挿入できると思います。 [私の答えのNTロードセクションを参照](http://stackoverflow.com/questions/35516878/acquire-release-semantics-with-non-temporal-stores-on-x64)。また、OPの問題サイズは6250 * 16浮動* 4B /浮動はL2キャッシュサイズの2/3にすぎないことに注意してください。おそらくキャッシュをバイパスすることは悪い考えです。 –