2016年1月6日水曜日

[gcc, spu] spu版gcc最適化問題(2)

2004年作成

変数を増やした場合、使用するレジスタが少なくなる


以下のようなサンプルプログラムを作って使用するレジスタ数を比較してみました。
プログラムはperlスクリプトで自動生成されます。

% perl t22.pl <変数の数>
struct s {
int a;
int b;
};
struct s a000;
struct s a001;
struct s a002;
:
int f()
{
int a = 0;
int b = 0;
a += (a000.a ^ a001.b);
a += (a001.a ^ a002.b);
:
a += (a145.a ^ a146.b);
a += (a146.a ^ a000.b);
b += (a000.b ^ a146.a);
b += (a146.b ^ a145.a);
:
b += (a002.b ^ a001.a);
b += (a001.b ^ a000.a);
return a + b;
}

このプログラムを-O3でコンパイルして使用するレジスタ数を調査しました。
結果は次のようになります。

変数の数 レジスタの数
80 87
90 97
100 107
110 117
120 127
130 127
140 127
147 127
148 123
149 116
150 113
160 86
170 66
180 55
190 43
200 44

変数の数が 147 までは使用できるレジスタを最大限に使うのですが、それからはレ
ジスタを使用しなくなって最終的に 43,44個に落ち着きます。

命令の順番を代えることによってサイクル数を少なくする


     1 sub(char *a, char *b, char *c, int n)
     2 {
     3  int i;
     4      5  for (i = 0; i < n; i++) {
     6    c[i] = a[i] + b[i];
     7  }
     8 }

このソースを-O3でコンパイルすると以下のようになります。

     6 sub:
     7 ori $11,$3,0
     8 cgti $3,$6,0
     9 ori $12,$6,0
    10 ori $10,$4,0
    11 ori $9,$5,0
    12 hbra .L9,.L6
    13 lnop
    14 nop $127
    15 il $8,0
    16 brz $3,.L8
    17 .L6:
    18 a $4,$11,$8 ; FX even 2
    19 lqx $5,$10,$8 ; LS odd 5
    20 a $14,$10,$8 ; FX even 2
    21 lqx $2,$11,$8 ; LS odd 5
    22 ai $6,$4,13 ; FX even 2
    23 lqx $13,$9,$8 ; LS odd 5
    24 ai $3,$14,13 ; FX even 2
    25 cbx $14,$9,$8 ; SH odd 4
    26 rotqby $7,$2,$6 ; SH odd 4
    27 rotqby $6,$5,$3 ; SH odd 4
    28 ah $5,$7,$6
    29 shufb $4,$5,$13,$14
    30 stqx $4,$9,$8
    31 ai $8,$8,1
    32 cgt $3,$12,$8
    33 .L9:
    34 brnz $3,.L6
    35 .L8:

(18,19),(20,21),(22,23),(24,25)がそれぞれ同じサイクルで実行されます。
26 で参照している $2 の準備のために 3 サイクル (22,24 で 2 サイクルですが LS では 5 サイクル必要なため) 無駄になります。
19 行目と 21 行目を交換するか、26,27 行目を交換 (この場合、レジスタ $6 の置き換えが必要) をすれば、無駄なサイクルは 2 サイクルに減ります。

上記のような命令順序の調整でサイクル数を削減することが可能です。

整数演算の順序


     1 int foo2(int a, int b, int c, int d)
     2 {
     3  return a + b + c + d;
     4      5 }

上記の関数を-O3でコンパイルすると

     1 foo2:
     2 a $7,$3,$4
     3 a $2,$7,$5
     4 a $3,$2,$6
     5 bi $lr

のようになります。

これは 2 行目の結果を 3 行目で、3 行目の結果を 4 行目で使用するため、二つの無駄なサイクルが発生します。
ここでは (((a+b)+c)+d) の順序で計算していますが、整数の加算・乗算の場合、順序は関係無いので、((a+b)+(c+d)) のように計算しても問題ありません。
この変更によって無駄なサイクルを減らすことが出来ます。(以下のパターンだと無駄なサイクル数が 1 になります)

     1 foo2:
     2 a $7,$3,$4
     3 a $2,$5,$6
     4 a $3,$2,$7
     5 bi $lr

乗算の場合は加算よりも効果があります。

     1 long foo1(long a, long b, long c, long d)
     2 {
     3  return a * b * c * d;
     4 }

上記の関数をコンパイルした場合:

     1 foo1:
     2 mpyh $14,$3,$4
     3 mpyh $7,$4,$3
     4 mpyu $13,$3,$4
     5 a $12,$14,$7
     6 a $11,$12,$13
     7 hbr .L2,$lr
     8 mpyh $10,$11,$5
     9 nop $127
    10 mpyh $2,$5,$11
    11 mpyu $9,$11,$5
    12 a $5,$10,$2
    13 a $4,$5,$9
    14 mpyh $3,$6,$4
    15 mpyh $8,$4,$6
    16 mpyu $7,$4,$6
    17 a $5,$8,$3
    18 a $3,$5,$7

変更後
     1 foo1:
     2 mpyh $14,$3,$4
     3 mpyh $7,$4,$3
     4 mpyu $13,$3,$4
     5 mpyh $8,$5,$6
     6 mpyh $9,$6,$5
     7 mpyu $10,$5,$6
     8 hbr .L2,$lr
     9 a $12,$14,$7
    10 a $15,$8,$9
    11 a $11,$12,$13
    12 a $16,$15,$10
    13 mpyh $13,$11,$16
    14 mpyh $14,$16,$11
    15 mpyu $15,$11,$16
    16 a $5,$13,$14
    17 a $3,$5,$15

計算回数は代わりませんが、乗算だけを詰めることで無駄なサイクルがなくなります。
この場合、9サイクル早くなりました。括弧で順序付けすることでも最適化されます。

減算、除算等にも使用できます。

無駄な型変換


     1 char foo4(char a, char b, char c, char d)
     2 {
     3  return a + b + c + d;
     4 }

上記のソースを-O3でコンパイルすると以下のアセンブラが生成されます。

     1 foo4:
     2 nop $127
     3 hbr .L6,$lr
     4 ah $7,$3,$4
     5 ah $4,$5,$7
     6 ah $2,$6,$4
     7 xsbh $3,$2
     8 xshw $2,$3
     9 ori $3,$2,0
    10 nop $127
    11 nop $127
    12 nop $127
    13 .L6:
    14 bi $lr

7行目でhalf wordからbyteへの型変換をやっていますので、8行目の処理が無駄です。

除算


定数がある場合。

     1 foo(int a)
     2 {
     3  return a / 3;
     4 }

これを-O3でコンパイルすると以下のようになります。
定数部分の最適化がされていません。

     1 .file "c.c"
     2 .text
     3 .align 3
     4 .global foo
     5 .type foo, @function
     6 foo:
     7 il $2,3
     8 lnop
     9 heqi $2,0 不必要
    10 hbrr 3f,1f
    11 sfi $10,$3,0
    12 sfi $11,$2,0
    13 cgti $12,$3,-1
    14 cgti $13,$2,-1 事前計算可能
    15 selb $10,$10,$3,$12
    16 selb $11,$11,$2,$13 事前計算可能
    17 xor $13,$12,$13
    18 clz $9,$11 事前計算可能
    19 clz $6,$10
    20 il $7,1
    21 sf $9,$6,$9
    22 ori $4,$10,0
    23 shl $6,$11,$9
    24 shl $7,$7,$9
    25 il $5,0
    26 1: clgt $8,$6,$4
    27 sf $9,$6,$4
    28 or $14,$5,$7
    29 rotmi $7,$7,-1
    30 rotmi $6,$6,-1
    31 selb $4,$9,$4,$8
    32 selb $5,$14,$5,$8
    33 3: brnz $7,1b
    34 2: sfi $10,$4,0
    35 sfi $11,$5,0
    36 selb $4,$10,$4,$12
    37 selb $5,$5,$11,$13
    38 ori $3,$5,0
    39 nop $127
    40 bi $lr

switch文


switch 文は case 毎にコードブロックを作成しテーブルを使ってそこに間接ジャンプするようになっています。
以下の特殊な場合、処理自体をまとめることで、無駄なジャンプを削ることが可能です。
1. 同じ変数への定数代入が多い。
2. 定数のリターン処理が多い。

     1 foo(unsigned short a)
     2 {
     3  switch(a) {
     4  case 0: return 1;
     5  case 1: return 2;
     6  case 2: return 3;
     7  case 3: return 4;
     8  case 4: return 5;
     9  default: return 0;
    10  }
    11 }

これをコンパイルすると以下のようになります。

     6 foo:
     7 ila $2,65535
     8 il $5,0
     9 and $4,$3,$2
    10 clgti $3,$4,4
    11 nop $127
    12 brnz $3,.L1
    13 shli $7,$4,2
    14 ila $3,.L9
    15 a $6,$7,$3
    16 lqd $5,0($6)
    17 rotqby $4,$5,$6
    18 bi $4
    19 .align 2
    20 .align 2
    21 .L9:
    22 .word .L3
    23 .word .L4
    24 .word .L5
    25 .word .L6
    26 .word .L7
    27 .L3:
    28 il $5,1
    29 br .L1
    30 .L4:
    31 il $5,2
    32 br .L1
    33 .L5:
    34 il $5,3
    35 br .L1
    36 .L6:
    37 il $5,4
    38 br .L1
    39 .L7:
    40 il $5,5
    41 lnop
    42 .L1:
    43 ori $3,$5,0
    44 bi $lr

この場合、case ブロックへのジャンプと case ブロックから脱出するためのジャンプの 2 回ジャンプが存在しますが、以下のようにすると 1 回ですみます。

     6 foo:
     7 ila $2,65535
     8 il $5,0
     9 and $4,$3,$2
    10 clgti $3,$4,4
    11 nop $127
    12 brnz $3,.L1
    13 shli $7,$4,2
    14 ila $3,.L9
    15 a $6,$7,$3
    16 lqd $5,0($6)
    17 .L1:
    18 ori $3,$5,0
    19 bi $lr
    20 .align 2
    21 .align 2
    22 .L9:
    23 .word 1
    24 .word 2
    25 .word 3
    26 .word 4
    27 .word 5

無効命令が削除されない


     1 foo(int a)
     2 {
     3  if (a > 0) {
     4    return 0;
     5  }
     6  if (a > 100) {
     7    return 100;
     8  }
     9  return -1;
    10 }

上記の命令では 7 行目の処理が行なわれることはありませんが、アセンブラファイルにはその処理が入っています。

     6 foo:
     7 cgti $9,$3,100
     8 hbr .L4,$lr
     9 cgti $5,$3,0
    10 nop $127
    11 ceqi $8,$9,0
    12 ceqi $2,$5,0
    13 ori $7,$8,100
    14 il $6,0
    15 selb $5,$6,$7,$2
    16 ori $3,$5,0
    17 nop $127
    18 .L4:
    19 bi $lr

マージ処理


条件分岐でまとめられる処理を SELB で一括処理しようとしていますが、しきれていません。

     1 foo(int a, int b, int c)
     2 {
     3  if (a > b) {
     4    a++;
     5    b++;
     6    c++;
     7  } else {
     8    a++;
     9    b--;
    10    c++;
    11  }
    12  return a + b + c;
    13 }

上記のソースでは 4 行目と 8 行目、6 行目と 11 行目がそれぞれ同じものになっています。
これをコンパイルすると、以下のようになります。

     6 foo:
     7 cgt $13,$3,$4
     8 hbr .L4,$lr
     9 ori $2,$5,0
    10 nop $127
    11 ai $7,$3,1
    12 ceqi $5,$13,0
    13 ai $10,$4,-1
    14 ai $14,$4,1
    15 selb $12,$7,$7,$5
    16 selb $4,$14,$10,$5
    17 ai $11,$2,1
    18 ceqi $8,$13,0
    19 a $5,$12,$4
    20 selb $7,$11,$11,$8
    21 a $3,$5,$7
    22 .L4:
    23 bi $lr

selb を使ってまとめて処理しようとしていますが、15 行目と 20 行目の処理でもselb を使用しています。この部分はコピー処理と等価です。

0 件のコメント:

コメントを投稿