Compiler Construction Lecture 12/7

Compiler Construction Lecture 12/7

先週の復習

先週は、簡単なコンパイラを作成した。そして、そのコンパイラを 中間木を生成する方法に変換した。

今週の課題は、問3について、

Subject: compiler report 11/20
というSubjectのメールにして、kono@ie.u-ryukyu.ac.jp まで送ること。 また授業を受けなかったものは、問1についても、
Subject: compiler lecture 11/20
というサブジェクトで送ること。また、Micro-C のソースリストを配布した ので、それも取りに来ること。

レジスタを使ったコンパイル

では簡単なコンパイラとその構成を調べて見よう。

Simple Compilerの構成

  Makefile          どのようにcompileするかの指示
  s-compile.c       compiler本体(interpreterとほとんど同じ)
  s-code-09.c       Code Generator
  ending.09         Code Generator Additional File
  s-code-rs6k.c     Code Generator for RS6000 (etc...)
  s-token.c         Tokenizer
  s-tree-compile.c  Intermidiate Tree Version



Simple CompilerのMakefile

必ず Makefile を 作り、make を使うことにしよう。つまらないタイプミスから開放される。 以下は Makefile の一部である。実行するコマンドの部分はタブで始める ことに注意よう。

TEST = s-09
CC = cc
COMPILER = s-compile.o s-token.o
CFLAGS = -g 
MC09EMU = ../emulator/util09/v09
MC09ASM = ../emulator/util09/a09
test-09:    s-09
        ./s-09 < s-input.txt > s-output.a09
        $(MC09ASM) s-output.a09
        $(MC09EMU) s-output
s-09:     s-compile.h $(COMPILER) s-code-09.o  
        $(CC) $(CFLAGS)  -o s-09 $(COMPILER) s-code-09.o 

test:     $(TEST)
        $(TEST) < s-input.txt > s-output.s
        $(CC) s-output.s
        a.out
s-rs6k-r:   s-compile.h $(COMPILER) s-code-rs6k-r.o  
        $(CC) $(CFLAGS)  -o s-rs6k-r $(COMPILER) s-code-rs6k-r.o 
ここで、make test-09 とすることにより、MC6809 emulator を使った テストをすることができる。

最初の6行は変数の定義。ここではMC6809のemulatorとassemblerを指定している。 その後の$(COMPILER)などは最初に指定した変数で置き換えられる。

3行目では test-09 の作り方を説明している。:のすぐ後には、s-09 つまり、 s-09 が test09 を作るために必要なことを示している。s-09 の作り方は、 最後の2行に書いてある。ここでも : の後に、s-09 を作るための原料が書いてある。 s-compile.o と s-compile.h と s-code-09.o が必要である。*.o は object file であり、*.c があれば、それから自動的に作られる(と make は考えて いる。実際には、これらの暗黙のルールがライブラリなどに置いてある)。

実際の作り方は、: の行のあとにタブを先頭に持つ行を続けて書く。スペース ではいけない。良くある間違いなので気を付けよう。この 行が指定され変数を置換された後、 sh に渡されて実行される。test-09 では、s-09 が既に作られていれば、それを使ってs-input.txt をcompile して、assembler で assemble し、結果を emulator で emulate する。

make は、Makefile を参照しながら、ファイルの時間を見る。そして、生成 されたものよりも、新しい変更時間を持つ原料があれば、その生成をやり直す。 もちろん、原料の作り直しが必要かどうかも自動的に判断する。これは、 一種の interpreter である。



問1

Makefile の中で、

TEST = s-rs6k-r
COMPILER = s-compile-tree.o s-token.o
とすると、rs6k の tree version が生成されてテストされる。すべての*.o が、まだ作成されていない時に、make test とすると、どのような順序で、 どのようなコマンドが実行されるかを記述せよ。そのコマンドが成功したとして 、さらにs-compile-tree.c を編集したする。その後、make test としたら、 コマンドはどのような順序で実行されるかを記述せよ。また、なぜ、その 順序で実行されたのかを Makefile の中のルールに合わせて、 簡単に説明せよ。



コンパイラの変更について

もし、8進、16進を許すだけならば、変更はs-compile.cだけで良い。実は これがもっとも簡単な変更である。

例えば、論理演算 & と | を付け加えたいとしよう。Parser(構文解析)部分の 変更は、Interpeterとの場合とほとんど変わらない。例えば、+ と同じ レベルに|を付け加えるとすると、(これは安易すぎる不便な構文である)

void
aexpr()
{
    int d;

    emit_comment();
    mexpr();
    while(last_token!=EOF) {
        switch(last_token) {
        case '|': 
            emit_push();
            mexpr(); 
            emit_calc(O_OR);
            break;
        case '&': 
            emit_push();
            mexpr(); 
O_ORは s-compile.h で定義する。ここで OR を計算するcodeを生成しなければ ならない。そのためには、code generator emit_calc を拡張しなくてはならない。

void
emit_calc(op)
int op;
{
    if(op==O_SUB) {   
        printf("\t%s ,S++\n",opcode[op]);
        printf("\tNEGA\n");
        printf("\tNEGB\n");
        printf("\tSBCA #0\n");
    } else if(op==O_ADD) {
        printf("\t%s ,S++\n",opcode[op]);
    } else if(op==O_SUB_R) {
        printf("\tsubd ,S++\n");
などとなっているので、ここにORをおこなうcodeを付け加えれば良い。 ANDD ,S++ というのがあればO_ADDなどと同じ処理で良い。しかし、 残念ながら6809には、そういう命令はない。(前回の付録参照)

しかし、ちょっと考えれば引き算と同様、Register AとRegister Bを 順に計算すれば良いことがわかる。Stack 上の順番は、Bigendian である ことを考えれば、

      ORA   ,S+
      ORB   ,S+
という code を出せば良い。(S++でなく、S+であることに注意) このためには、
    } else if(op==O_OR) {
        printf("\tORA ,S+\n");
        printf("\tORB ,S+\n");
とすれば良い。ANDに関しても、全く同じように実現できる。

このように MC6809では、D or A,B というAccumulatorとMemory間で計算を行うのが 普通である。これは、Register-Memory Computation 中心の Architecture である。一方、Registerをたくさん持っている場合は、まず必要なデータを loadし、Register-Register Computation をおこない、その後、結果を store するというのが一般的である。これは、Load-Store Architecture と呼ばれる。

例題のdirectoryにある compile.c では、さらにいろいろな拡張を試みている。 参考にして見よう。



Register based compiler

今までの簡単なコンパイラでは、Register-Memory Architecture を使い、 Register 1つと、Stackの先頭の演算に式をコンパイルしていた。ここでは、 さらに、Load-Store Architecture を想定して、Register 上のでの演算に コンパイルして見よう。

最近ではRegisterの数は32が多く、その中で自由に使えるものは10-20程度 ある。通常の式であれば、スタックの深さは10以下であり、すべてRegister 上で計算できる。この部分を記述するのは簡単である。もっと難しいのは、 複数の式でRegisterを共有する場合であり、この時には、共有する変数は 非常に大量になる。これに対して適切なRegister割当をおこなうのがコンパイラ の最適化として重要である。この割当の善し悪しは、関数呼出時の手間や Cache(キャッシュ)の 占有度に影響し、最終的にプログラム全体のPerformanceに影響する。この 問題は、授業の後半で議論することにしよう。

さて、Registerを使った式の計算は、スタックに変数を取っておく代わりに Registerに取っておくだけである。したがって、コンパイラを変更しないで、 コード生成系のみを変更するようにしよう Register 割当をおこなう s-code-sparc.c を見てみよう。

emit_push() でスタックにaccumlatorの値をsaveする代わりに、新しく accumlator として使う Register を割り当てれば良い。このために、 get_register() という手続きを考える。


char *reg_name[] = {
    "%g0","%g1","%g2","%g3","%g4","%g5","%g6","%g7",
    "%i0","%i1","%i2","%i3","%i4","%i5","%i6","%i7",
    "%l0","%l1","%l2","%l3","%l4","%l5","%l6","%l7",
    "%o0","%o1","%o2","%o3","%o4","%o5","%o6","%o7"
};
#define O0RN 24
#define MAX_REGISTER 32

char regs[MAX_REGISTER];
char reg_stack[MAX_REGISTER];

int
get_register() {
    int i;
    for(i=0;i <MAX_REGISTER;i++) {
        if (! regs[i]) {
            regs[i]=1;
            return i;
        }
    }
    error("Expression too complex (register overflow)");
    return -1;
}
regs[]という配列に、そのRegisterが使われているかどうかを示すflagを いれておく。get_register()では、最初のflagが開いているRegisterの 番号を返す。もちろん、番号とassemblerの中でのRegister名との対応を 示すものが必要なので、それは、reigster_name[]という配列を作って おく。このうちいくつかは、最初から使用不可(システムやarchitectureが 使用している、例えば、SP, RTOC など)である。

どのRegisterをsaveに使ったかはコンパイラが自分で覚えてなくてはいけない。 これにはスタックを使う。これは、ちょうど実行時のスタックをコンパイル時に 部分計算していると考えることもできる。先に計算できる部分なわけである。 accumlatorを表す Registerの番号を current_register としよう。

void
emit_push()
{
    reg_stack[reg_sp++] =  current_register;
    lrn = reg_name[current_register];
    current_register = get_register();
    crn = reg_name[current_register];
}
ここで、lrn は last regster name, crn は current register name につもり である。どうせ、すぐ使うのだから、ここで代入しておこう。emit_load, emit_store, emit_value は、current_register に値をいれるだけなので、以下のように簡単に 書くことができる。

void
emit_value(d) 
int d;
{
    if(-4096 < d && d < 4096) {
        printf("\tmov %d,%s\n",d,crn);
    } else {
        printf("\tsethi %%hi(%d),%s\n",d,crn);
        printf("\tor %s,%%lo(%d),%s\n",crn,d,crn);
    }
}


void
emit_push()
{
    reg_stack[reg_sp++] =  current_register;
    lrn = reg_name[current_register];
    current_register = get_register();
    crn = reg_name[current_register];
}


void
emit_load(assign)
int assign;
{
    printf("\tsethi %%hi(_variable+%d),%%g2\n",assign*4);
    printf("\tld [%%g2+%%lo(_variable+%d)],%s\n",assign*4,crn);
}


void
emit_store(assign)
int assign;
{
    printf("\tsethi %%hi(_variable+%d),%%g2\n",assign*4);
    printf("\tst %s,[%%g2+%%lo(_variable+%d)]\n",crn,assign*4);
}



実際の計算を行うところでは、stackを一つ下げることをおこなう必要が ある。ここではRegister-Register演算をおこない、入らなくなった Registerの regs[] に 0 を入れれば良い。これは pop_register() という手続きにしよう。


void
emit_calc(op)
int op;
{
    char *orn;
    orn = pop_register();
    if(op==O_MUL) {
        printf("\tmov %s,%%o1\n",lrn);
        printf("\tcall .umul,0\n");
        printf("\tmov %s,%%o0\n",orn);
        printf("\tmov %%o0,%s\n",crn);
    } else if(op==O_DIV) {
        printf("\tmov %s,%%o0\n",lrn);
        printf("\tcall .div,0\n");
        printf("\tmov %s,%%o1\n",orn);
        printf("\tmov %%o0,%s\n",crn);
    } else if(op==O_DIV_R) {
        printf("\tmov %s,%%o0\n",orn);
        printf("\tcall .div,0\n");
        printf("\tmov %s,%%o1\n",lrn);
        printf("\tmov %%o0,%s\n",crn);
    } else if(op==O_SUB) {
        printf("\t%s %s,%s,%s\n", opcode[op], lrn, orn, crn);
    } else {
        printf("\t%s %s,%s,%s\n", opcode[op], orn, lrn, crn);
    }
}


char *
pop_register() {
    int i,j;

    j = current_register;
    i = reg_stack[--reg_sp];
    lrn = reg_name[i];

    regs[j]=0;

    current_register = i;
    crn = reg_name[i];
    return reg_name[j];
}

ここでは、スタックを使う場合のように、最後の結果が最初のRegisterに なるように工夫している。

これにより、スタック上での演算よりも確実に高速なコードを出すことが できる。例えば、 0+(1+(2+(3+(4+(5+(6+(7+8)))))))-(0+(1+(2+(3+(4+(5+(6+(7+8)))))))) のような場合である。しかし、これを 0+1+2+3+4+5+6+7+8-(0+1+2+3+4+5+6+7+8)と書き換えるとReisterは一つ しか使わないで済む。もちろん、中間木を使ってコンパイル時に定数計算を してしまえば、もっと簡単になる。

このような書き換えは簡単だと思うかも知れないが、実際には同等な式への 変換は無数にあり、もっともRegisterが少なくなるような式の変形は自明 ではない。このような変形を行うためには中間木への変換が必ず必要となる。 このような式の変換に付いても後で議論することにしよう。

問2

0+(1+(2+3))-(0+(1+(2+3))) の式の計算を上の2種類のcompiler によりSPARCの命令に変換した結果を記述せよ。0+1+2+3-0+1+2+3 の場合はどうなるか?

問3

s-code-sparc.c を使った、 s-sparc について、論理演算 &,| を拡張して見よう。余裕が あれば他の拡張にも挑戦して見よう。



Micro-Cの全体構成

これから、Micro-C をざっと読みながら、より詳細な技術について解説して いく。そのまえに、Micro-C の全体構成をざっと見て見よう。

Micor-C の全体構成