目次
gcc -mtune=hoge ってあんまよくわからず付けてるけど意味あんの?
前のk10 vs coremaみたいな感じでparsecをひととおり動かした
-O2 の時間を1.0とした時の処理時間の比 app |-O2 -mtune-native streamcluster | 1.016 canneal | 1.016 vips | 0.954 bodytrack | 0.966 x264 | 1.049 blackscholes | 0.986 swaptions | 0.981 ferret | 0.997
なんらかの効果がある…ように見える…?
(ネタバレ: -march=avxが付いてて、それの効果が出ている)
をやっている
GCC にはスケジュールする機能があって -fschedule-insns, -fschedule-insns2 で有効になる。 無印はレジスタ割り当ての前、2はレジスタ割り当てのあとに処理される。 普通、-O2でアーキテクチャにあった適切ほうが有効になる。 x86はレジスタが少ないのでschedule-insns2だけ有効になる。
スケジューリングのアルゴリズムは、
みたいになる。(途中の情報は-fsched-verbose=100とかで見れる)
この 3. の、パイプラインをシミュレートするときに、 命令のレイテンシやスーパースカラのパイプ占有量が必要になる。 それが、gcc/config/i386/core2.md とかに書いてある。 -mtune を変えれば、どれを使うかが変わる。
が、そもそもCore2以降みたいにメモリアクセスもリオーダーするプロセッサがあった時、 コンパイラのスケジューリングに意味があるかどうかが謎なので、効果はよくわからない。
例えば、手元のi5-2400S で、↓のコードを、
float data0[512]; float data1[512]; float data2[512]; float data3[512]; float data4[512]; float data5[512]; float data6[512]; float data7[512]; int func(float *f, float *d, int n, double x) { int i; for (i=0; i<n; i++) { float c = data1[i] + data2[i]; float d = data2[i] + data3[i]; float e = c+d; float f = d+e; data0[i] += ((c+e)+ (c+d+e))+ (d+e+c+f); } }
で、それぞれコンパイルした場合の結果を実行しても、全く違いが見られない。
さすがにAtomだと、-fno-schedule-insns2付けると変わるけど、-mtune=core2と-mtune=atomで違いが出なかった。 違いが出る例もあると思うけど、簡単に調べた範囲だと見つからず。
x86は伝統的に、「〜は遅いから使うな」みたいな命令があって、例えば、 x86最適化のアレ具合を面白おかしく紹介する場合によく引き合いに出されるincとかがあるのだが…
全盛期の inc/dec 伝説 (半分ネタなので内容は信じないでください)
これが、アーキテクチャによって挙動が変わるので、サイズとの兼ね合いでinc,decになったりする
int v(int n) { int i; int sum0 = 0; int sum1 = 4; int sum2 = 8; for (i=0; i<n; i++) { sum0 ++; sum1 ++; sum2 ++; asm volatile (" ":"+r"(sum0), "+r"(sum1), "+r"(sum2)); } return sum0 + sum1 + sum2; }
# -mtune = core2 .L3: addl $1, %ecx addl $1, %edx addl $1, %eax #APP # 13 "inc.c" 1 # 0 "" 2 #NO_APP addl $1, %ebx cmpl %esi, %ebx jne .L3 addl %ecx, %edx addl %edx, %eax
# -mtune = bdver1 .L3: incl %ecx incl %edx incl %eax #APP # 13 "inc.c" 1 # 0 "" 2 #NO_APP incl %ebx cmpl %esi, %ebx jne .L3 addl %ecx, %edx addl %edx, %eax
こういう調整が入る。
どうなっているかというと、gcc/config/i386/i386.hに
enum ix86_tune_indices { X86_TUNE_USE_LEAVE, ... X86_TUNE_REASSOC_INT_TO_PARALLEL, X86_TUNE_REASSOC_FP_TO_PARALLEL, X86_TUNE_LAST };
こういうのが書いてあって、gcc/config/i386/i386.cに、アーキテクチャとの対応が書いてある。
いくつか面白いと思ったもの/調べるのが簡単だったのを抜き出しておくと
上で説明したinc/decの問題。フラグレジスタの一部を更新した命令を使ったあとに、 フラグレジスタ全体を参照すると余計なストールが発生する or uopsが発生するアーキではinc/decを使わないためにある。
Atomは数命令程度の短い関数だと、余計なストールが発生する(なので、PICのために、callしてespを取るみたいなのが遅い)。 これを無くすために、短い関数に、nopを入れる
; void f() { return ; } : gcc -O2 -mtune=atom f: .LFB0: .cfi_startproc nop nop nop nop nop nop nop nop ret .cfi_endproc
自動ベクタライザで、AVXが使える場合でも128bit演算を使う。Bulldozerでは、AVX256bit速くないんだろうか…(未確認)。
float a[128]; float c[128]; void b(int n) { int i; for (i=0; i<n; i++) { a[i]+=c[i]; } } ;; gcc -O2 -ftree-vectorize -mtune=bdver1 -march=bdver1 .L5: vmovaps a(%rax), %xmm1 incl %edx vaddps c(%rax), %xmm1, %xmm0 vmovaps %xmm0, a(%rax) addq $16, %rax cmpl %edx, %ecx ja .L5 ;; gcc -O2 -ftree-vectorize -mtune=corei7-avx -march=bdver1 .L5: vmovaps a(%rax), %ymm0 addl $1, %edx vaddps c(%rax), %ymm0, %ymm0 vmovaps %ymm0, a(%rax) addq $32, %rax cmpl %edx, %ecx ja .L5
倍精度処理を自動でベクタライズしない。Atom は倍精度はSIMD命令使ったほうが遅い。
double a0[128]; double a1[128]; void b2(int n) { int i; for (i=0; i<n; i++) { a0[i]+=a1[i]; } } ;; -O2 -ftree-vectorize .L18: movapd a0(%rax), %xmm0 addl $1, %edx addpd a1(%rax), %xmm0 movapd %xmm0, a0(%rax) addq $16, %rax cmpl %edx, %ecx ja .L18 ;; -O2 -ftree-vectorize -mtune=atom .L17: movsd a0(%rax), %xmm0 addsd a1(%rax), %xmm0 movsd %xmm0, a0(%rax) leaq 8(%rax), %rax cmpq %rdx, %rax jne .L17
SSEのスカラ演算は、スカラ以外の部分をゼロクリアするもの、しないものがあるが、 X86は128bit演算が64bitx2 だった頃の影響で、結構めんどい場合がある
そのあたりをケアする
__attribute__((noinline,noclone)) int func(float *f, double *d, int n, double x) { int i; for (i=0; i<n; i++) { f[i] = f[i] + 1.1; } asm ("":::"memory"); } ;; -O2 -mtune=pentium4 -mfpmath=sse .L3: cvtss2sd (%eax), %xmm0 addsd %xmm1, %xmm0 cvtsd2ss %xmm0, %xmm0 movss %xmm0, (%eax) addl $4, %eax cmpl %edx, %eax jne .L3 ;; -O2 -mtune=core2 -mfpmath=sse .L3: movss (%eax), %xmm0 cvtps2pd %xmm0, %xmm0 addsd %xmm1, %xmm0 unpcklpd %xmm0, %xmm0 cvtpd2ps %xmm0, %xmm0 movss %xmm0, (%eax) addl $4, %eax cmpl %edx, %eax jne .L3
簡単なループを実験で-ftree-vectorizeすると、あんま速くならない場合結構あるのだが、これは、
アラインわからないからmovaps使えない → movups 遅い → movlpsとmovhpsで半分ずつロード/ストア → 命令数増える
という感じなのだった。
最近のCPUはmovups遅くないので、アラインわからなくても(わかっても!)素直にmovups使えばよい。
void b3(float *a, float *b, int n) { int i; for (i=0; i<n; i++) { a[i] += b[i]; } } ;; -O2 -ftree-vectorize -mtune=core2 .L30: movaps %xmm2, %xmm0 movaps %xmm2, %xmm1 addl $1, %r8d movlps (%rsi,%rax), %xmm1 movlps (%rdi,%rax), %xmm0 movhps 8(%rsi,%rax), %xmm1 movhps 8(%rdi,%rax), %xmm0 addps %xmm1, %xmm0 movlps %xmm0, (%rdi,%rax) movhps %xmm0, 8(%rdi,%rax) addq $16, %rax cmpl %r8d, %ecx ja .L30 ;; 2.0 @ i5-2400 ;; 3.67 @ Atom N550 ;; -O2 -ftree-vectorize -mtune=corei7 .L30: movups (%rdi,%rax), %xmm0 addl $1, %r8d movups (%rsi,%rax), %xmm1 addps %xmm1, %xmm0 movups %xmm0, (%rdi,%rax) addq $16, %rax cmpl %r8d, %r9d ja .L30 ;; 0.63 @ i5-2400 ;; 6.6 @ Atom N550 ;; -O2 .L5: movss (%rdi,%rax,4), %xmm0 addss (%rsi,%rax,4), %xmm0 movss %xmm0, (%rdi,%rax,4) addq $1, %rax cmpl %eax, %edx jg .L5 ;; 2.0 @ i5-2400 ;; 8.1 @ Atom N550
LEA 便利だが、AGU <--> ALU で値を行き来するとレイテンシ増えるので、いつでも使ってよいわけではない。 けど、AtomはAGUとALUがくっついているので全くペナルティ無いのだった。
int a(int a, int b, int x, int y) { return (a + b + x + y); } ;; gcc -O2 -mtune=atom a: .LFB0: .cfi_startproc leal (%rdi,%rsi), %edi leal (%rdi,%rdx), %edx leal (%rdx,%rcx), %eax nop nop ret ;; gcc -O2 -mtune=atom a: .LFB0: .cfi_startproc leal (%rdi,%rsi), %edi leal (%rdi,%rdx), %edx leal (%rdx,%rcx), %eax nop nop ret ;; gcc -O2 -mtune=core2 a: .LFB0: .cfi_startproc addl %esi, %edi addl %edi, %edx leal (%rdx,%rcx), %eax ret
まあ、以上の話を見た感じ、ものによっては少しぐらい速くなりそうで、遅くなることは無いだろう…というように読み取れる。実際、
-O2 の時間を1.0とした時の処理時間の比 == time == <1回目> app | O2 -mtune-native| streamcluster | 1.016| canneal | 1.016| vips | 0.954| bodytrack | 0.966| x264 | 1.049| blackscholes | 0.986| swaptions | 0.981| ferret | 0.997| <2回目> app | -O2 -mtune=native| streamcluster | 1.016| canneal | 1.018| vips | 0.929| bodytrack | 0.978| x264 | 0.988| blackscholes | 0.992| swaptions | 0.980| ferret | 0.996|
という感じで、まあ、そういうように見える。
とりあえず一番効果が大きいように見えるvipsを見ておく…ん…
3.68 : 488345: c4 c1 7a 10 34 89 vmovss (%r9,%rcx,4),%xmm6 0.80 : 48834b: c5 f8 5a ff vcvtps2pd %xmm7,%xmm7 1.83 : 48834f: c5 f8 5a f6 vcvtps2pd %xmm6,%xmm6 3.13 : 488353: c4 c1 43 59 fa vmulsd %xmm10,%xmm7,%xmm7 4.72 : 488358: c4 c1 4b 59 f3 vmulsd %xmm11,%xmm6,%xmm6 0.86 : 48835d: c5 c3 58 fe vaddsd %xmm6,%xmm7,%xmm7 4.91 : 488361: c4 c1 7a 10 34 8a vmovss (%r10,%rcx,4),%xmm6 0.07 : 488367: c5 f8 5a f6 vcvtps2pd %xmm6,%xmm6 0.03 : 48836b: c4 c1 4b 59 f4 vmulsd %xmm12,%xmm6,%xmm6 2.73 : 488370: c5 c3 58 fe vaddsd %xmm6,%xmm7,%xmm7 1.81 : 488374: c4 c1 7a 10 34 8b vmovss (%r11,%rcx,4),%xmm6 0.02 : 48837a: c5 f8 5a f6 vcvtps2pd %xmm6,%xmm6 1.92 : 48837e: c4 c1 4b 59 f5 vmulsd %xmm13,%xmm6,%xmm6 0.94 : 488383: c5 c3 58 f6 vaddsd %xmm6,%xmm7,%xmm6 1.81 : 488387: c5 fb 12 f6 vmovddup %xmm6,%xmm6 1.66 : 48838b: c5 f9 5a f6 vcvtpd2ps %xmm6,%xmm6
(あ!これAVXだ…)
やりなおして…
== time == app | -O2 -mtune=native vips | 0.994
。。。変わらなくね。。。?
他のを見るか…もうこれほぼ一緒にしか見えないが…
== cycles == app | -O2 -mtune=native streamcluster | 1.004 canneal | 1.015 vips | 0.997 bodytrack | 0.999 x264 | 1.009 blackscholes | 0.999 swaptions | 1.017 ferret | 0.994
ということは、-mtune=nativeは効いてないが、AVX の3オペランドは、数% 程度の効果はあったということか!!
AVXの3オペランドは、効果があるので使ったほうがいい。
…もうちょっと真面目にまとめると…
GCCの-mtuneは、CPUアーキテクチャの弱点を回避するために、有効な場面がありそうに見える。
ただ、プロセッサは、進歩するごとに弱点も減っていくので、「最新のプロセッサに最新のコンパイラで適切なオプションで最強!」みたいな 幻想は抱かないほうがいいように見える。
2012/3/31 : この文章は、第3回 x86/x64最適化勉強会のために書かれました