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は手元に無いので実験できない…)