目次
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最適化勉強会のために書かれました