プログラマのためのHaswell入門

(上の生データやらグラフを見て情報読み取れる人は読まなくてよいです)


さて、世間のレビュー見てると、Haswellいらん子感があるけど、HaswellはIvyと比べて性能2倍になっている点はプログラム書く人は知っておくべきだと思う。
(IntelのCPUはCore2以降デスクトップ向け強化とサーバー/HPC向け強化を交互にやってる感があって、今回はHPC向けターンかなーという気がする)


Haswellで性能出すために知っておくべきことなどを書いておく。


Haswellで強化された点

プログラム書く場合に考慮すべきHaswellの強化点は、

  • FMA命令が追加/FMULx1/FADDx1 が FMAx2 になった
  • AVX2命令が追加
  • 整数演算パイプ追加
  • TSX

あたりかと思う。以下、それぞれどう意識すればいいか解説する。

(TSXは使えないCPUを選んでしまったので特に解説しません)


FMA命令が追加/FMULx1/FADDx1 が FMAx2 になった

まず、一番重要な点が、Haswellでは、浮動小数演算性能が倍になっているという点。


FMA(Fused Multiply and Accumulate)は、積和演算といって乗算と加算を同時にやる命令で、なんで同時にやるの?かというと、まあ僕もあんま理由知らないけど、同時にやると嬉しい場合はそれなりにあって、例えば、畳み込みフィルタだと、

out[y][x] = in[y-1][x-1]*coeff[0] + in[y-1][x]*coeff[1] + ... in[y+1][x+1]*coeff[8]

こんな感じ、で、まあ、こんなコード書かないけど、という人もたくさんいるかもしれないが、うーん…
あとTop500で使うLinpackは最内が行列積でそこでよく使うとかだと通じるかな…


まあ、ともかく、わからない人も、積和演算が重要だと仮定を置いて続きを読んで欲しい。


そういうFMAが実装された、と。


Ivyまでは、1cycleあたり、浮動小数の加算(_mm256_add_ps)と乗算(_mm256_mul_ps)が一回ずつできた。Haswellではこの部分が大幅に強化されて、1cycleあたり、FMAが二回できるようになっている。
FMAは一回で2回の演算を行うので、

  • Ivy : 1cycleあたり2回の演算(2FLOP)
  • Haswell : 1cycleあたり4回の演算(4FLOP)

と、倍になっている。まあ、これは、理論値で、実際には、常に積和をやるわけではなくて、加算だけ、乗算だけ、という場合もたくさんあるので、常に2倍になるわけではない。まあ、プログラムによっては最大二倍になる可能性はある、ということで。(乗算命令は倍になっているので、乗算だけのプログラムだと二倍になる可能性はある。何故か加算は変わってない)


コンパイラによってはmul_psとadd_psを使うだけでfmaが出ると風の噂で聞いたことがあるが、手元で試した限りGCC/VCはそういうことはやらないようなのでintrinsic手で書きましょう。

#include <immintrin.h>
#include <stdio.h>

float x = 2;
float y = 3;
float z = 1;

int 
main()
{
    float fv;

    __m256 a, b, c, d;

    a = _mm256_set1_ps(x);
    b = _mm256_set1_ps(y);
    c = _mm256_set1_ps(z);

    //d = _mm256_add_ps(_mm256_mul_ps(a,b), c);
    d = _mm256_fmadd_ps(a, b, c);

    fv = _mm_cvtss_f32(_mm256_castps256_ps128(d));

    printf("%f\n", fv);
}

こんな感じで、_mm256_fmadd_ps/_mm256_fmadd_pd を使うとよい。


注意としては、fmaは0.5bit精度が良いので、値によっては結果が変わる場合がある。


あと出る命令に少し注意が必要で、上のをVCに入れると、

	vfmadd231ps ymm2, ymm0, ymm1

こんなのが出る。231って何?


世の中の大半の命令は入力2オペランド、出力1オペランドで問題が解決するので、AVX命令のフォーマットは、レジスタ3つしか取らないようになっているのだが、このFMAは、入力を3オペランド必要とする。


で、多分この命令のためだけに命令フォーマット拡張したくなかったんだと思うが、FMA命令は、3入力オペランドのうち、一個どれを破壊するかを指定できるようになっていて、そのために3種類の命令がある。

  • vfmadd132
  • vfmadd213
  • vfmadd231

http://news.mynavi.jp/photo/articles/2012/10/03/idf_haswell_hpc_01/images/002l.jpg

どれが出力だっけ。まあ間違ったこと書きそうだから自分で調べて。普通はintrinsicで書くと思うのでまあ気にしないでいいと思う。


まあ、そんなわけで、HaswellはFMAを使うと本来の性能を発揮できるという話であった。
(Bulldozerも似たようなこと言ってた)


あとこのへんの強化にあわせてL1の帯域も倍になっている。L1に収まる範囲では帯域が足りないということはないでしょう。
逆に言うと、L1より大きいデータだとあんま速くならない可能性がある。



AVX2

AVXは、浮動小数しか無くて整数系の人達はガッカリだったわけだが、Haswellで整数も256bitになった。


上で書いたように、

    m128:      padd:   latency: CPI=    1.00, IPC=    1.00
    m128:      padd:throughput: CPI=    0.50, IPC=    2.00
    m256:     paddd:   latency: CPI=    1.00, IPC=    1.00
    m256:     paddd:throughput: CPI=    0.50, IPC=    2.00

きちんと命令スループット出てるので、整数性能も倍になっていると言っていいだろう。


使いかたは…SSE2以降使ったことある人なら、avx2intrin.h 見れば大体わかるのではないかな…(面倒になってきた)
http://clang.llvm.org/doxygen/avx2intrin_8h_source.html


特におもしろ命令は…無い。無いですよ。gatherなんてなかった。


まあ一応解説しておくと、皆が待ち望んだgather命令というのが増えている。


待ち望んでいたというのはかなり本当で、散らばってるデータに対して同じ処理をやりたい場合、SSE/AVXだと、

  1. 一個ずつデータをロードしてmmレジスタに詰める(gather)
  2. それに対して演算
  3. mmレジスタの値をバラバラにする(scatter)

とかやらないといけないのだが、この前後の処理が多くて、SIMDにしたほうが遅くなる事例がいっぱいあった。CUDAと比較して性能負ける原因のひとつがこれと言ってもいいくらいだった。


それをなんとかするのが、このgather命令である。
http://software.intel.com/en-us/blogs/2011/06/13/haswell-new-instruction-descriptions-now-available/
このへんに解説がある。

_mm256_i32gather_epi32 intrinsic関数を使えばよくて、

    int v0 = mem[i0];
    int v1 = mem[i1];
    int v2 = mem[i2];
    int v3 = mem[i3];
    int v4 = mem[i4];
    int v5 = mem[i5];
    int v6 = mem[i6];
    int v7 = mem[i7];

    __m128i lo = _mm_cvtsi32_si128(v0);
    lo = _mm_insert_epi32(lo, v1, 1);
    lo = _mm_insert_epi32(lo, v2, 2);
    lo = _mm_insert_epi32(lo, v3, 3);
    __m128i hi = _mm_cvtsi32_si128(v4);
    hi = _mm_insert_epi32(hi, v5, 1);
    hi = _mm_insert_epi32(hi, v6, 2);
    hi = _mm_insert_epi32(hi, v7, 3);

こういうのが、

    __m256i pattern = _mm256_set_epi32(i0, i1, i2, i3, i4, i5, i6, i7);
    __m256i data = _mm256_i32gather_epi32(mem, pattern, 4);

こう書けるようになる。(まあこれは例が良くない。patternは別のSIMD演算で出てきた結果だと思って)


これによって、問題によっては劇的な改善がされることが期待されていた。が、IDF2013で、

Relative high latency and low throughput on Haswell (Haswellでは比較的ゆっくりです)

Will get improvement in Intel(R) next generation CPU (Intel先生の次回作にご期待ください)

とか公式に明言されてて世界中をガッカリの渦に巻きこんだ。


実際性能見るとこのぐらい違う

gather: [ 0, 4, 8,12,16,20,24,28]: 68.279663
insert: [ 0, 4, 8,12,16,20,24,28]: 39.467285

あと出してるのはインデックスのパターンだが、連続アクセスだからなんか最適化とか、そういう空気は一切感じられない。

gather: [ 0, 8,16,24,32,40,48,56]: 68.245972
insert: [ 0, 8,16,24,32,40,48,56]: 36.012817
gather: [ 0, 1, 2, 3, 4, 5, 6, 7]: 68.246704
insert: [ 0, 1, 2, 3, 4, 5, 6, 7]: 48.155273
gather: [ 0,16,32,48,64,80,96,112]: 68.210449
insert: [ 0,16,32,48,64,80,96,112]: 36.268433
gather: [ 0,32,64,96,128,160,192,224]: 68.238281
insert: [ 0,32,64,96,128,160,192,224]: 36.318604
gather: [ 0,128,256,384,512,640,768,896]: 68.216675
insert: [ 0,128,256,384,512,640,768,896]: 36.013184

Haswellでは、いつでも、常に、gatherを使うべきではない。

scatterは…まだ調べてないけど…まあいいか…

整数演算パイプ追加

地味な改善だが、スカラの整数演算パイプが追加されてる。


SIMD演算をやりながら、アドレス計算できるようにしてある。(と、どっかで読んだ気がする)

まとめ

まあこれだけ色々強化されていてもoverallベンチマークでは全然性能出ないの大変興味深いですね。

haswell

とりあえず手元にツールある分くらいはのせとく

https://github.com/tanakamura/instruction-bench

http://int.main.jp/prog/bin/bench.exe

ターボブースト止めかたわからんかったので信頼性は若干よくない。まあaddのレイテンシ1.00になってるし多分あってる。

アンロール8回程度ではmulx2のレイテンシを埋められない問題が発覚してるのでIPC変な値になってるやつはソース確認してください。

== latency/throughput ==
   reg64:       add:   latency: CPI=    1.00, IPC=    1.00
   reg64:       add:throughput: CPI=    0.29, IPC=    3.49
   reg64:      load:   latency: CPI=    5.00, IPC=    0.20
   reg64:      load:throughput: CPI=    0.63, IPC=    1.60
    m128:      pxor:   latency: CPI=    0.28, IPC=    3.55
    m128:      pxor:throughput: CPI=    0.28, IPC=    3.55
    m128:      padd:   latency: CPI=    1.01, IPC=    0.99
    m128:      padd:throughput: CPI=    0.50, IPC=    2.00
    m128:    pmuldq:   latency: CPI=    5.00, IPC=    0.20
    m128:    pmuldq:throughput: CPI=    1.00, IPC=    1.00
    m128:    loadps:   latency: CPI=    7.01, IPC=    0.14
    m128:    loadps:throughput: CPI=    0.50, IPC=    2.00
    m128:     xorps:   latency: CPI=    0.28, IPC=    3.55
    m128:     xorps:throughput: CPI=    0.28, IPC=    3.56
    m128:     addps:   latency: CPI=    3.00, IPC=    0.33
    m128:     addps:throughput: CPI=    1.00, IPC=    1.00
    m128:     mulps:   latency: CPI=    5.03, IPC=    0.20
    m128:     mulps:throughput: CPI=    0.63, IPC=    1.59
    m128:   blendps:   latency: CPI=    1.00, IPC=    1.00
    m128:   blendps:throughput: CPI=    0.33, IPC=    3.00
    m128:    pshufb:   latency: CPI=    1.00, IPC=    1.00
    m128:    pshufb:throughput: CPI=    1.00, IPC=    1.00
    m128:    pmullw:   latency: CPI=    5.02, IPC=    0.20
    m128:    pmullw:throughput: CPI=    1.00, IPC=    1.00
    m128:    phaddd:   latency: CPI=    3.00, IPC=    0.33
    m128:    phaddd:throughput: CPI=    2.00, IPC=    0.50
    m128:    pinsrd:   latency: CPI=    2.06, IPC=    0.49
    m128:    pinsrd:throughput: CPI=    2.00, IPC=    0.50
    m128:      dpps:   latency: CPI=   14.03, IPC=    0.07
    m128:      dpps:throughput: CPI=    2.00, IPC=    0.50
    m128:  cvtps2dq:   latency: CPI=    3.00, IPC=    0.33
    m128:  cvtps2dq:throughput: CPI=    1.00, IPC=    1.00
    m256:    loadps:   latency: CPI=    1.00, IPC=    1.00
    m256:    loadps:throughput: CPI=    0.50, IPC=    2.00
    m256:     xorps:   latency: CPI=    0.28, IPC=    3.54
    m256:     xorps:throughput: CPI=    0.28, IPC=    3.56
    m256:     mulps:   latency: CPI=    5.03, IPC=    0.20
    m256:     mulps:throughput: CPI=    0.63, IPC=    1.58
    m256:     addps:   latency: CPI=    3.01, IPC=    0.33
    m256:     addps:throughput: CPI=    1.00, IPC=    1.00
    m256:     divps:   latency: CPI=   18.11, IPC=    0.06
    m256:     divps:throughput: CPI=   14.13, IPC=    0.07
    m256:     divpd:   latency: CPI=   19.11, IPC=    0.05
    m256:     divpd:throughput: CPI=   16.14, IPC=    0.06
    m256:   rsqrtps:   latency: CPI=    7.00, IPC=    0.14
    m256:   rsqrtps:throughput: CPI=    2.00, IPC=    0.50
    m256:     rcpps:   latency: CPI=    7.01, IPC=    0.14
    m256:     rcpps:throughput: CPI=    2.00, IPC=    0.50
    m256:    sqrtps:   latency: CPI=   18.13, IPC=    0.06
    m256:    sqrtps:throughput: CPI=   14.22, IPC=    0.07
    m256:vperm2f128:   latency: CPI=    3.00, IPC=    0.33
    m256:vperm2f128:throughput: CPI=    1.00, IPC=    1.00


https://github.com/tanakamura/clminibench

http://int.main.jp/prog/bin/clinstbench.exe

使いかた全く説明してないが、bench.c の文字コードSJISにして(Androidから使う場合はutf-8にして)jni/w32以下のCMakeLists.txtをビルドすればよいです。

「おそい」「はやい」とかのコメントは全く意味ない(人間ごときがコンピュータの性能を速い遅いとか評価していいのかという精神)ので無視してよいです。

TSX

http://ark.intel.com/products/75117/Intel-Core-i7-4700MQ-Processor-6M-Cache-up-to-3_40-GHz

ごめんTSX付いてないやつだった。誰かにまかせる。

vgather

#include <immintrin.h>
#include <x86intrin.h>
#include <stdio.h>

__attribute__((aligned(64))) unsigned int zero_mem[4096];

__m256i data_zero;
volatile __m256i result;

template <int scale>
__attribute__((noinline, noclone))
static void
bench_gather(int i0, int i1, int i2, int i3,
             int i4, int i5, int i6, int i7,
             int nloop)
{
    __m256i pattern = _mm256_set_epi32(i0, i1, i2, i3, i4, i5, i6, i7);
    __m256i zero = data_zero;
    __int64 b = __rdtsc();

    for (int i=0; i<nloop; i++) {
        __m256i data = _mm256_i32gather_epi32((const int *)zero_mem, pattern, scale);

        data = _mm256_and_si256(zero, data);
        pattern = _mm256_add_epi32(pattern, data);
    }

    result = pattern;

    __int64 e = __rdtsc();

    printf("gather: [%2d,%2d,%2d,%2d,%2d,%2d,%2d,%2d]: %f\n",
           i0*scale, i1*scale, i2*scale, i3*scale, i4*scale, i5*scale, i6*scale, i7*scale,
           (e-b)/(double)nloop);
}

void
__attribute__((noinline, noclone))
bench_ins(int i0, int i1, int i2, int i3,
          int i4, int i5, int i6, int i7,
          int nloop, int scale)
{
    __int64 b = __rdtsc();

    for (int i=0; i<nloop; i++) {
        int v0 = zero_mem[i0*scale];
        int v1 = zero_mem[i1*scale];
        int v2 = zero_mem[i2*scale];
        int v3 = zero_mem[i3*scale];
        int v4 = zero_mem[i4*scale];
        int v5 = zero_mem[i5*scale];
        int v6 = zero_mem[i6*scale];
        int v7 = zero_mem[i7*scale];

        __m128i lo = _mm_cvtsi32_si128(v0);
        lo = _mm_insert_epi32(lo, v1, 1);
        lo = _mm_insert_epi32(lo, v2, 2);
        lo = _mm_insert_epi32(lo, v3, 3);
        __m128i hi = _mm_cvtsi32_si128(v4);
        hi = _mm_insert_epi32(hi, v5, 1);
        hi = _mm_insert_epi32(hi, v6, 2);
        hi = _mm_insert_epi32(hi, v7, 3);

        __m256i v = _mm256_castsi128_si256(lo);
        v = _mm256_inserti128_si256(v, hi, 1);

        ((__m256i*)zero_mem)[0] = v;
    }

    __int64 e = __rdtsc();

    printf("insert: [%2d,%2d,%2d,%2d,%2d,%2d,%2d,%2d]: %f\n",
           i0*scale, i1*scale, i2*scale, i3*scale, i4*scale, i5*scale, i6*scale, i7*scale,
           (e-b)/(double)nloop);
}

template <int scale>
static void
bench(int i0, int i1, int i2, int i3,
      int i4, int i5, int i6, int i7,
      int nloop)
{
    bench_gather<scale>(i0, i1, i2, i3, i4, i5, i6, i7, nloop);
    bench_ins(i0, i1, i2, i3, i4, i5, i6, i7, nloop, scale);
}


int main()
{
    int nloop = 2048*16;
    bench<4>(0,1,2,3,4,5,6,7, nloop);
    bench<8>(0,1,2,3,4,5,6,7, nloop);
    bench<1>(0,1,2,3,4,5,6,7, nloop);

    bench<4>(4*0,4*1,
             4*2,4*3,
             4*4,4*5,
             4*6,4*7,
             nloop);

    bench<1>(32*0,32*1,
             32*2,32*3,
             32*4,32*5,
             32*6,32*7,nloop);

    bench<1>(128*0,128*1,
             128*2,128*3,
             128*4,128*5,
             128*6,128*7,nloop);

}

やったーvgather命令vinsrdで実装しなおしたらはやくなったよー。

gather: [ 0, 4, 8,12,16,20,24,28]: 68.279663
insert: [ 0, 4, 8,12,16,20,24,28]: 39.467285
gather: [ 0, 8,16,24,32,40,48,56]: 68.245972
insert: [ 0, 8,16,24,32,40,48,56]: 36.012817
gather: [ 0, 1, 2, 3, 4, 5, 6, 7]: 68.246704
insert: [ 0, 1, 2, 3, 4, 5, 6, 7]: 48.155273
gather: [ 0,16,32,48,64,80,96,112]: 68.210449
insert: [ 0,16,32,48,64,80,96,112]: 36.268433
gather: [ 0,32,64,96,128,160,192,224]: 68.238281
insert: [ 0,32,64,96,128,160,192,224]: 36.318604
gather: [ 0,128,256,384,512,640,768,896]: 68.216675
insert: [ 0,128,256,384,512,640,768,896]: 36.013184

avx2/fma

とりあえず生ログだが

    m256:      pxor:   latency: CPI=    0.28, IPC=    3.56
    m256:      pxor:throughput: CPI=    0.28, IPC=    3.56
    m256:     paddd:   latency: CPI=    1.00, IPC=    1.00
    m256:     paddd:throughput: CPI=    0.50, IPC=    2.00
    m256:   vpermps:   latency: CPI=    3.00, IPC=    0.33
    m256:   vpermps:throughput: CPI=    1.00, IPC=    1.00
    m256:   vpermpd:   latency: CPI=    3.00, IPC=    0.33
    m256:   vpermpd:throughput: CPI=    1.00, IPC=    1.00
    m256:    vfmaps:   latency: CPI=    5.00, IPC=    0.20
    m256:    vfmaps:throughput: CPI=    0.50, IPC=    2.00
    m256:    vfmapd:   latency: CPI=    5.00, IPC=    0.20
    m256:    vfmapd:throughput: CPI=    0.50, IPC=    2.00

上のIPCのやつ適当にグラフも描いといたから各自見といて。
https://skydrive.live.com/view.aspx?resid=ECB59E566C2D71F1!1173&cid=ecb59e566c2d71f1&app=Excel&authkey=!AI3dvNyGgQ4hkWs

追記: スループット/レイテンシは公式情報も出てます

http://www.intel.com/content/www/us/en/architecture-and-technology/64-ia-32-architectures-optimization-manual.html

一致してない値もあるけど多分原因調べたりはしないので、そっから先は必要な人が各自調べてくだしあ。