今週は Target Macro の記述です。Target Macro は Machine Description に書けないような種々の情報をすべて含んでおり、非常に重要な役割を果たしています。
まずはじめに、ワード長やレジスタ数、種々のアラインメント、エンディアンなどを #define で定義していきます。TSU は 16 ビットの汎用レジスタを r0 から r15 まで 16 個持っていますが、そのうち r15 は CPU 内部でゴミ箱として利用されており、値が不意に変化することがあるので、gcc では特に明示したとき以外使わないようにしました。スタックポインタには専用のレジスタが必要なので、r14 をその目的に割り当てます。フレームポインタや引数ポインタには、それぞれ r12 と r11 を使うことにしました。ただし、これらは専用のレジスタである必要がなく、通常はスタックポインタで代替できるので、これによって汎用のレジスタが減るわけではありません。i386 や SPARC のような CPU もあるので、レジスタの能力やレジスタ・ウィンドウに関する記述も可能ですが、TSU ではどちらも必要ないので定義しませんでした。
それから、アセンブリ・コードの出力に関するマクロを定義します。ここにはレジスタの名前やラベルの表現から、コード領域とデータ領域の分け方、static 変数の領域確保、switch - case で用いるテーブルの出力、デバッグ情報の記述まで、大変細かく指定することができます。TSU のアセンブラ tas は普通のアセンブラと比べてかなり変わったところが多いのですが、gcc の Target Macro が世の中のありとあらゆるアセンブラを想定しているおかげで、tas を変更する必要はほとんど生じませんでした。
tm の中で一番重要なのが、関数呼び出しの形式とスタック・フレームの配置に関する記述です。この部分は VAX から SPARC まで CPU によって実に様々な方式があるため、マクロの記述も非常に自由度が高くなっています。TSU には特に関数呼び出しをサポートする命令はありませんので、ここはいろいろと考える余地のあるところです。
最初に考えたのはスタック・フレームの構造です。TSU では load や store などでメモリアドレスを指定する際、レジスタの値にオフセットを加えたものを用います。しかし、このオフセットには小さな非負の定数しか指定できません。このためスタックはアドレスの小さいほうへ成長することにしました。
次に引数の渡し方を決めます。古典的な CPU やコンパイラでは、引数はすべてスタックに push するのが普通でしょう。しかし TSU の場合、学科から提供されたメモリが非常に低速でメモリアクセスのコストが大きいので、可能な限りレジスタ渡しを用いることにします。
最後にレジスタの値をどのように退避するかを考えないといけません。最近のコンパイラでは関数をこえた global register allocation を行うものも多いですが、(関数全体が inline 化される場合は別として)gcc はそのような最適化は行いません。したがって、レジスタの保存は caller-save か callee-save のどちらかを採用するのが普通です。一般にどちらの方式が良いのかは難しい問題で一概にはいえないと思います。ただ TSU の場合は、浮動小数演算をソフトウェアによるサブルーチン呼び出しで行うため、caller-save だと破壊されないレジスタまで保存される無駄が大きいので、callee-save を採用することにしました。
以上をまとめると、関数の呼び出しは次のように行われることになります。
関数からの return は次の通りです。
ただし実際のプログラムでは、可変個の引数や細かな効率上の問題も考慮して、もう少し違った処理を行っています。これらは一つのマクロで #define するには複雑すぎるので、local.c において tsu_function_prologue() と tsu_function_epilogue() という二つの関数でコードを出力しています。
ここまできてようやく、実際に C や C++ のソースをコンパイルできるようになりました。しかし md や tm の記述が不適切だったり、普通の CPU では発覚しなかった gcc 本体のバグがあったりして、当然ですがデバッグにかなり手間がかかりました。特に本質的な問題はなかったので、ここで一つ一つのバグを説明はしませんが、一つだけ強く印象に残ったことがあります。gcc 本体にかなり多くの assertion が含まれているおかげで、途中でおかしな状態になると自分から abort してくれるのです。これはデバッグをする上でかなり助かりました。
結局この週は、ほとんどの時間をデバッグに費やしました。デバッグの過程でソースを熟読せざるを得なかったので、gcc の仕組みをだいたい理解することができたというおまけもあります。
この時期は最初の発表会とコンテストがあって、メインの作業が大詰めを向かえていたので、TSU-GCC のほうにはあまり手をつけられませんでした。コンテストはレイトレと CPU の動いていた班が他になかったため、自然に我々が優勝しました。発表会のほうは、プロセッサ実験に対する僕の「提言」が大受けして、とても盛り上がりました(笑)。詳しくは 発表で使った OHP をごらんください。
今週は後回しになっていた浮動小数演算を扱いました。TSU は FPU を持たないため、浮動小数演算はソフトウェアによるサブルーチンで行います。 担当者 の努力と才能のおかげで、TSU の浮動小数演算ルーチンは非常に高速でした。演算ルーチンの内部については彼のレポートにまかせることにして、ここではコンパイラとのインターフェースについて述べたいと思います。
一般に浮動小数演算ルーチンというと、通常の関数呼び出しと同じように、特定のレジスタに引数をセットして呼び出すと、決まったレジスタに戻り値が返ってくるものです。コンパイラ演習の講義でもそのような形が暗黙に仮定されていました。しかし、この方法では引数を準備したり戻り値を使ったりするのにオーバーヘッドが生じます。そこで TSU-GCC では、引数や戻り値には任意のレジスタを使って良いことにし、すべての組み合わせに対して別々の演算ルーチンを用意しました。
ただ、それを素朴に実装すると、コードサイズが大きくなりすぎてメモリが足りなくなってしまいます。そのため、演算ルーチンのコードはできる限り共有するとともに、加算と乗算は可換性を用いて引数の組み合わせを半分に減らした上で、結果的に一度も使われていないルーチンはリンクしないようにしました。これによって、64 KB しかない TSU のコード用メモリに、レイトレのプログラムを何とか収めることができました。
さて、引数と戻り値に任意のレジスタを割り当てられるならば、ハードウェアによる浮動小数演算命令もソフトウェアによる浮動小数演算ルーチンも、コンパイラの上位層にとっては同じようなものです。したがって、gcc の提供する一般的な枠組みを普通に利用できます。
ただし、実際にはやはりいくつか特殊な問題を解決する必要がありました。まず、TSU の浮動小数演算ルーチンは内部の演算にいくつかのレジスタを必要とし、一部のルーチンは引数も破壊します。そこで、浮動小数演算の中間コードを生成するときには、作業に用いるレジスタと引数に使うレジスタを新しく確保し、引数は必ずコピーしてから用いるようにしました。ここで要求するのはあくまで仮想レジスタなので、実際にコピーする必要がない場合は、実レジスタの割り当ての際に無駄が省かれるようになっています。
それから、同じ数を足したりかけたりするときには、単にその数を二倍したり二乗したりすれば良いだけなので、特別扱いすることによって高速化できます。これは中間コードを生成するときに、引数の中身を調べることによって簡単に実現できました。
なお、演算ルーチンの実行は通常の関数呼び出しと同様に、戻り番地を r14 に保存してから jump を行います。jump 命令には 3 つの delay slot がありますが、そのうちの 2 つは r14 のセットで埋まります。したがって、浮動小数演算を行うマクロ命令 fcall は、普通の call と同じく delay slot を 1 つ持ちます。
最終的に浮動小数演算を生成する部分は、以下のようになりました。この例は加算のものですが、他の演算も同じように記述できます。
(define_insn "tsu_add_double" [(set (match_operand:HF 0 "register_operand" "=&r") (plus:HF (match_operand:HF 1 "register_operand" "%r") (match_operand:HF 2 "register_operand" "r"))) (clobber (reg:QI 13)) (clobber (match_dup 1)) (clobber (match_dup 2)) (clobber (match_scratch:QI 3 "=&r")) (clobber (match_scratch:QI 4 "=&r"))] "" "* { if (REGNO (operands [1]) < REGNO (operands [2])) { if (REGNO (operands [3]) < REGNO (operands [4])) { return tsu_fill_delay_slot (1, \"fcall\\tfadd@%1@%2@%0@%3@%4\"); } else { return tsu_fill_delay_slot (1, \"fcall\\tfadd@%1@%2@%0@%4@%3\"); } } else { if (REGNO (operands [3]) < REGNO (operands [4])) { return tsu_fill_delay_slot (1, \"fcall\\tfadd@%2@%1@%0@%3@%4\"); } else { return tsu_fill_delay_slot (1, \"fcall\\tfadd@%2@%1@%0@%4@%3\"); } } }" [(set_attr "length" "6") (set_attr "type" "t_call")]) (define_expand "addhf3" [(set (match_operand:HF 0 "register_operand" "=r") (plus:HF (match_operand:HF 1 "register_operand" "%r") (match_operand:HF 2 "register_operand" "r")))] "" " { rtx src1, src2; if (tsu_same_register (operands [1], operands [2])) { src1 = gen_reg_rtx (HFmode); emit_insn (gen_tsu_mov_double (src1, operands [1])); emit_insn (gen_tsu_dbl_double (operands [0], src1)); DONE; } src1 = gen_reg_rtx (HFmode); src2 = gen_reg_rtx (HFmode); emit_insn (gen_tsu_mov_double (src1, operands [1])); emit_insn (gen_tsu_mov_double (src2, operands [2])); emit_insn (gen_tsu_add_double (operands [0], src1, src2)); DONE; }")