Compiler Construction Lecture 1/25

Compiler Construction Lecture 1/25

先週の復習

先週は、以下のことを勉強した。

手続き呼び出し



Register WindowとDelayed Slot

SPARCでは、変わった呼び出し方をしている。SPARCには overraped register window というのがあり、それぞれ、%i,%l,%o というように区別されている。 それぞれ8本ずつある。call すると、caller の %o が、callee の%i となる。 これを引数渡しに使う。(したがって8個以上の引数はregister渡しにできない。 実際にはframe pointerやstack pointerがあるので、もっと少ない) また、浮動小数点には、regisger windowはない。 calleeからcallerに戻る時には、再び%iが%oとなる。これによって値を 返すことができる。この切替えは、save/restore という命令によって おこなわれる。

この方法の良いところは、stackにいちいち値をcopyしなくても済むところ である。しかし、register windowは無限にあるわけではないので、いつかは いっぱいになってしまう。その時にはsub routineを呼び出して、register windowの中身をスタックにcopyする。copyした後、戻る時には、また、スタック からregister windowにcopyする。実際のアプリケーションでは、この register window overflowが頻繁に起きるので、 残念ながら、この方法が成功しているとは いえなかったようである。

実際には以下のような呼び出しを用いる。

        mov 4,%g3
        call _print,0
        mov %g3,%o0        callより、こちらが先に実行される
L1:
        ret
        restore            register window をもとにもどす
        .align 8
        .global _print
        .proc   04
_print:
        !#PROLOGUE# 0
        save %sp,-104,%sp        register window の切替え
        !#PROLOGUE# 1
        st %i0,[%fp+68]          %i0にcallerの%o0が入っている
        sethi %hi(LC0),%o1
        or %o1,%lo(LC0),%o0
        ld [%fp+68],%o1
        ld [%fp+68],%o2
        call _printf,0           この呼び出しはregister windowを使わない
        nop
L2:
        ret     
        restore                  ret より先にこちらが実行される

call の後にある、,0 と、nop は、Delayed Slot と呼ばれるもの である。昔のSparc は、jump や、call の際に、一つだけそのアド レスを先読みすることにより、パイプラインの乱れを吸収していた。 ここにコンパイラがnopではなく、実際の命令を置くことにより、 パイプラインを乱すことなく計算を続けることができる。

しかし、今のCPU技術では、命令の先読みは数命令に渡っており、 1つのDelayed Slotではパイプラインの吸収しきれない。代わって、 分岐予測が重要となっている。例えば、call文などでは、call 先が呼ばれることがわかっているので、先読みは、そちらを 行えば良い。ここでもSPARCのRISCの技術は残念ながら失敗している。

また、PowerPCなどでは、分岐を判断するフラグを分岐命令よりも 前に確定させることにより的確な先読みを可能にしている。フラグの 確定を先に持って行くのは、コンパイラの責任である。



基本ブロックとフローグラフ

これまでは、式(expression)単位のコンパイルを考えてきた。このコンパイルは、 直接的で簡単である。レジスタの数の少ない6809では最適に近いコードを出力 ることができる。しかし、最適化(optimize)を考える時、特にレジスタの数の 多いRISCの場合は、これではレジスタを有効利用しているとはいえない。

式の単位での最適化と、式を越えた大域的な最適化を考えるために、 基本ブロック(Basic Block)とフローグラフ(Flow Graph)というものが 工夫された。

基本単位は、単純に言えば、条件分岐や分岐を含まない逐次計算の 部分である。フローグラフは、一つの手続の中で基本ブロックを分 岐を結んだものである。

mc.c の reverse() はリストを逆順にするものだが、

   484	reverse(t1)
   485	int t1;
   486	{int t2,t3;
   487		t2=t1;
   488		while(type!=t1)
   489		{	t3=cadr(type);
   490			rplacad(type,t2);
   491			t2=type;
   492			type=t3;
   493		}
   494		type=t2;
   495	}
このcadr()とかrplcad()は
  2840	cadr(e)
  2841	int e;
  2842	{	return heap[e+1];
  2843	}
  2894	rplacad(e,n)
  2895	int e,n;
  2896	{	heap[e+1]=n;
  2897		return e;
  2898	}
である。すると、
   484	reverse(t1)
   485	int t1;
   486	{int t2,t3;
   487		t2=t1;
   488		while(type!=t1)
   489		{	t3=heap[type+1];
   490			heap[type+1]=t2;
   491			t2=type;
   492			type=t3;
   493		}
   494		type=t2;
   495	}
としてよい。

問題1

このフローグラフを考えてみよう。 このフローグラフ(flow graph)を作るには、まず基本ブロック(basic block) を抜きだす。基本ブロックは、分岐(barch)の入らない部分 のまとまりをさす。次に、flow-chart のように基本ブロックを接 続する。あと、入力される変数と、出力される変数を記述する。

flow-chart と違い、基本ブロックは最適化の単位となるので、可 能な限り一つにまとめなければならない。結果は以下のようになる。


基本ブロック内部では条件分岐がないので、文(statement)の実行 順序(execution order)をプログラムの意味を変えない範囲で変更 してコンパイルして良い。これは局所的最適化と呼ばれる。

基本ブロックの間で再利用出来る式、計算する必要のない式、ある いは、使わない変数などを見つけると、それらを共有したり、削っ たりすることができる。さらに、簡単なループの場合は内部の基本 ブロックの定数などをループの外に出すことも行われる。これらは 大域的最適化と呼ばれる。これらが終わると、変数にレジスタを割 り当てる(register allocation)。RISCでは、このレジスタの割り 当て方法が重要である。



基本ブロックの最適化 Optimization of basic block

基本ブロックの最適化では共通の式などを抜きだす。このためには、 基本ブロックの中の式を一度、DAG グラフ(Directed Acyclic Graph) に直すのが良い。

DAGの作り方

前のフローグラフの真ん中のDAGは以下のようになる。

DAGからコード生成する時には、単に上から生成していくというわ けにはいかない。ちゃんと計算可能な部分から計算し、代入する時 には、他の演算がその前の値を使おうとしていないかを確かめなく てはならない。

問題 1

以下のプログラムのDAGを作成せよ。できれば、作成したDAGにした がってi386 のアセンブラを作成せよ。

A:   a=b+c; b=a-d; c=b+c; d=a-d;
B:   a=b+c; b=b-d; c=c+d; e=b+c;
C:   e=a+b; f=e-c; g=f*d; h=a+b; i=i-c; j=i+g;
ヒント。単にグラフの形だけでなく、式の意味を考えて共通部分式と しても良い。



局所的最適化 Local Optimization

局所的最適化の代表的なものは peep hole optimization と呼ばれ るものである。これはいったんコードを生成した後に、そのコード を狭い範囲で見て、より良いものに置き換えられるならば置き換え るというものである。共通部分式の置き換えなどはできないが、簡 単で効果的であることが知られている。コード生成の時点で行って しまっても良い。例えば、micro-C ではそうなっている。



問題1のアセンブラに落すのを宿題にします。