目次

概要

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のコード解説

gcc/*.c
アーキ非依存のコード
gcc/config/i386/*
i386のコード
gcc/config/i386/i386.c
i386用の処理
gcc/config/i386/i386.h
i386用の定義
gcc/config/i386/i386.md
命令定義。これをgcc/gen*.c が読んで、insn-*.c を生成する
gcc/config/i386/{atom,k6,bdver1,core2,...}.md
i386.mdにincludeされる。CPU毎のパイプライン情報が書いてある

結局何やってんの?

をやっている

スケジューリング

GCC にはスケジュールする機能があって -fschedule-insns, -fschedule-insns2 で有効になる。 無印はレジスタ割り当ての前、2はレジスタ割り当てのあとに処理される。 普通、-O2でアーキテクチャにあった適切ほうが有効になる。 x86はレジスタが少ないのでschedule-insns2だけ有効になる。

スケジューリングのアルゴリズムは、

  1. 依存グラフを作る
  2. 依存が解決されているグラフのノードをキューに入れる
  3. パイプラインをシミュレートして、キューの中の命令で完了したものを解決したものとする

みたいになる。(途中の情報は-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);
    }
}
  1. 全くスケジューリングをしない場合 (-O2 -fno-schedule-insns2 -funroll-loops)
  2. Core2スケジューリングをする場合 (-O2 -mtune=core2 -funroll-loops)

で、それぞれコンパイルした場合の結果を実行しても、全く違いが見られない。

さすがに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に、アーキテクチャとの対応が書いてある。

いくつか面白いと思ったもの/調べるのが簡単だったのを抜き出しておくと

X86_TUNE_USE_INCDEC

上で説明したinc/decの問題。フラグレジスタの一部を更新した命令を使ったあとに、 フラグレジスタ全体を参照すると余計なストールが発生する or uopsが発生するアーキではinc/decを使わないためにある。

X86_TUNE_PAD_SHORT_FUNCTION

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

X86_TUNE_AVX128_OPTIMAL

自動ベクタライザで、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

X86_TUNE_VECTORIZE_DOUBLE

倍精度処理を自動でベクタライズしない。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

X86_TUNE_SSE_SPLIT_REGS, X86_TUNE_USE_VECTOR_FP_CONVERTS

SSEのスカラ演算は、スカラ以外の部分をゼロクリアするもの、しないものがあるが、 X86は128bit演算が64bitx2 だった頃の影響で、結構めんどい場合がある

  1. SSE 64bit アーキ : 上位をクリアすると0クリアuopsが生成されるので、もったいない
  2. SSE128bit アーキ : 上位をクリアしないと、レジスタの部分更新が発生して、必要の無い依存が発生する可能性がある

そのあたりをケアする

__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

X86_TUNE_SSE_UNALIGNED_LOAD_OPTIMAL, X86_TUNE_SSE_UNALIGNED_STORE_OPTIMAL

簡単なループを実験で-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

X86_TUNE_OPT_AGU

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アーキテクチャの弱点を回避するために、有効な場面がありそうに見える。

ただ、プロセッサは、進歩するごとに弱点も減っていくので、「最新のプロセッサに最新のコンパイラで適切なオプションで最強!」みたいな 幻想は抱かないほうがいいように見える。

参考文献

GCC

この文章について

2012/3/31 : この文章は、第3回 x86/x64最適化勉強会のために書かれました