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