AtomでHTを使ってピーク性能を向上させる

HyperThreadingというかSMTは最適化されたプログラムだと全く効果が無いという説があるが、そうでないこともある。


きっかけはAtomのmovdquが遅くて泣きたい、というようなのだった。

Intelの最適化マニュアルを見ると、movdquはレイテンシ3のスループット2とか書いてあんだが、実測すると全く信じられない。Agnerさんのinstruction tableのレイテンシ6、スループット6の方が正しいと思う。(スループットは逆数表示です)

スループット6というのがどのくらい酷いかというと、

void
vecadd(int *a,int *b,int *c,int n)
{
    int i;
    for (i=0; i<n; i++) {
        a[i] = b[i]+c[i];
    }
}

こういうのだと全然速くならんぐらいに酷い。

このプログラムは命令列は load,load,add,storeになるんだが、load,storeそれぞれ6cycleかかるようになるので、ループ一回あたり19cycleとかになって、スカラ演算を4回アンロールして実行する(12cycle?)ほうが速い。

Intelの最適化マニュアルには、Atomではmovdquよりpalignrとmovapsを使えとか書いてあるんだが、バイトずれごとのループを全部書き下せというのか貴様は。(いや、配列サイズが大きいなら、ループ実行前に適切なpalignrを生成するJITみたいなのもできそうだな。こんど必要に迫られたときやろう)


まあ、それはいいとして、上の計測をしていて気付いたのだが、movdquとかdivとかのレイテンシの長い命令(?)は、もう一個スレッドを立ててやることで、スループットを向上させることができるみたい。

例えばmovdquはシングルスレッドだと、どうスケジュールしてもスループット6なんだが、もう一個スレッド立ててやると、それぞれのスレッドでスループットが8になって、合計4まで下げられる。検証プログラムは以下。

#include <emmintrin.h>
#include <stdio.h>
#include <sys/time.h>
#include <pthread.h>
#include <stdint.h>

static inline unsigned int
rdtsc()
{
    unsigned int lo,hi;
    asm volatile ("rdtsc"
                  :"=a"(lo), "=d"(hi));

    return lo;
}

#define NTHREAD 8
__attribute__((aligned(16))) unsigned char array[NTHREAD][1024];

static void *
thread_func(void *p)
{
    int tid = (int)(intptr_t)p;
    double prev = 0;
    double loop_count = 0;
    while (1) {
	int i;

        unsigned int b,e;
        int n = 10000000;

        b = rdtsc();
	for (i=0; i<n; i++) {
#define G4(A) A A A A
	    asm(G4("movdqu 16*0+0(%0), %%xmm0\n\t"
                   "movdqu 16*1+0(%0), %%xmm1\n\t"
                   "movdqu 16*2+0(%0), %%xmm2\n\t"
                   "movdqu 16*3+0(%0), %%xmm3\n\t"
                   "movdqu 16*4+0(%0), %%xmm4\n\t"
                   "movdqu 16*5+0(%0), %%xmm5\n\t"
                   "movdqu 16*6+0(%0), %%xmm6\n\t"
                   "movdqu 16*7+0(%0), %%xmm7\n\t")
		:
		:"r"(array)
		:"xmm0", "xmm1", "xmm2", "xmm3", "xmm4", "xmm5", "xmm6", "xmm7"
		);

	}
        e = rdtsc();

        printf("throughput = %f[cycle/insn]\n",
               (e-b)/((double)n*4*8));
    }
}

int
main(int argc, char **argv)
{
    int nth = 1;
    pthread_t *threads;
    if (argc >= 2) {
        nth = atoi(argv[1]);
    }

    threads = (pthread_t*)malloc(nth*sizeof(pthread_t));
    for (int i=0; i<nth; i++) {
        pthread_create(&threads[i], NULL,
                       thread_func, (void*)i);
    }

    for (int i=0; i<nth; i++) {
        pthread_join(threads[i], NULL);
    }

    return 0;
}

結果がこんな感じ。引数はスレッド数。

$ ./loadbench 1
throughput = 6.061490[cycle/insn]
throughput = 6.090106[cycle/insn]
throughput = 6.074289[cycle/insn]

$ ./loadbench 2
throughput = 8.141643[cycle/insn]
throughput = 8.170663[cycle/insn]
throughput = 8.175771[cycle/insn]
throughput = 8.158954[cycle/insn]


まあ、だからどうしたという話だが…レイテンシ長い命令がボトルネックになってるときは、もう一個スレッド立てればなんとかなるかもしれん、という話であった。(Nehalemは手元に無いので実験できない…)

カジュアル並列プログラミングの世界へようこそ(という妄想)

movdquが遅いというのはひじょーに困る。カジュアル並列プログラミングの世界がまた一歩遠ざかってしまう。

並列化というのは、うまくやらないと遅くなる可能性があって、まあ、うまくやるといっても、そんなに大変ではないのだが、しかし、「遅くなるかも」という可能性が1%でもあるとき、作業を無駄にするリスクを背負わないといけない。これが精神に与えるダメージがでかい。この精神に与えるダメージが小さいとき、それをカジュアル並列プログラミングと呼ぶ(呼ばない)。

SSEは、CPUにくっついてる都合上、このリスクがかなり低く、カジュアルだと思っていたが、movdquがスループット6とかで辛い。


並列処理にも手軽度があって、SSEはかなり手軽な部類だ。末端のプログラムの末端の処理に、ちょこっと追加するみたいなことができる。例えば、

int a[16];
/* 初期化 */
for (int i=0; i<16; i++) {
   a[i] = 0;
}

暇なときにこういうのを書きかえるとかできる。


レイテンシがでかいとそうはいかない。いくつかの関数にまたがって処理を直さないといけない場合がある。例えば、x86だとコア間の通信が…えーと…忘れたのでどうでもいいや。



この点を踏まえて、AMDの提唱するFusionについて考えてみよう。
AMDのFusionはヘテロジニアスコンピューティングと呼ばれている仕組みであるが、実際出てみると、単にGPUくっつけただけでしたズコー。となることが予定されているCPUで、妄想するなら発表されていない今のうちしかないというものである。


さて、このFusionで搭載されるAPUがどうなってると嬉しいか。

今のSSEとかAVXは、ベクタ長が決まっているので、これが、ソフト、ハードを硬直させ気味である。
個人的には、これがなんとかなってくれると嬉しいと思う(いや、ほんまに嬉しいか…?)。すなわち、ソフトウェア的にはSIMDのベクタ長が可変長に見えて、それをうまくコプロセッサがなんとかする的な感じである。

    cpmov   counter, eax   # cpmov コプロ(=APU)mov
    cpmov   ptr0, ecx
    cpmov   ptr1, edx
    cpmov   ptr2, ebx
    cpmov   command, ADD
    cpsync                 # 終了まで待機

というような感じで、ベクタマシンを横にくっつけたみたいな感じにする。カウンタ値が大きいとキャッシュバイパスして、メモリバンド幅理論値近く出るみたいになってると理想。(Phenomとかはメモリバンド幅が理論値と比較して小さすぎて悲しいので)
んで、APUとの通信レイテンシは、数千cycleぐらいだと嬉しい。そのぐらいなら気軽に使えるという気がする。


さて、cpsyncした時に、OS的にコンテキストスイッチして、他のプロセスがAPUを使ったらどうなるだろうか。まあ、ここは妄想の世界なので特に考えない。(ポインタとカウンタぐらいなら退避復元できる気がするが)


しかし、実際これが欲しいか?というと微妙。SIMDSIMDとは言うが、実際SIMDが欲しいと言ったときに、狭義のSIMD(Single Instsruction Multiple Data)だけが欲しい場合はレアケースで、実際はみんなshuffleとかpextrwとかhaddとかしまくってんだろ?CUDAがHigh Performance Fortran(及びそれに類似するアレとかコレとか)みたいな美しい形をとらないで、大量のスレッドがそれぞれ動いちゃうみたいな仕組みになったのは、みんなそれが欲しかったからだろォ…

というように考えると、上で示したように、ポインタとカウンタとコマンドを設定してGoみたいな形では表現できず、コプロセッサ側にもそれなりに強力なフロー制御の仕組みが必要になって、命令カウンタと命令キャッシュを詰んで、ISAが定義され、ほげほげして、複雑になってしまって、CPUと距離がとられレイテンシがでかくなり、カジュアル並列プログラミングはさらに妄想の世界の奥深くへと旅立っていくのだった。

特にまとめとかは無い。