前のページ 次のページ 目次

6. 97 年 2 月

6.1 第 1 週

CPU のほうがほぼできあがったので、TSU-GCC のほうに取り組みはじめます。家の PC のほうが学校のワークステーションよりもずっと速いので、開発は普段のように自宅でやることにしました。

まずは、GNU の総本山ともいえる prep.ai.mit.edu に ftp して最新のバージョンを確認し、そのミラーがある tron.is.s.u-tokyo.ac.jp から gcc-2.7.2.2.tar.gz を get します。しかし、ファイルのサイズがあまりにも大きく、28.8 K bps で ftp する気にはなりませんでした。そこで、家にあった CD-ROM の山から gcc-2.7.2.tar.gz を探し出し tar xvzf してから、gcc-2.7.2-2.7.2.1.diff.gz と gcc-2.7.2.1-2.7.2.2.diff.gz をとってきて gunzip し patch -p1 しました。

さて、アーカイブを展開したら、次はお決まりの configure です。当たり前ですが TSU-GCC そのものは TSU ではなく PC のほうで動かすのですから、コンパイラを実行するマシン(ホスト)と生成するコードの対象となるマシン(ターゲット)は別々です。このようなコンパイラをクロス・コンパイラといいますが、クロス・コンパイラとして用いる gcc をクロス gcc と呼びます。クロス gcc を make するには、configure に「--target=hogehoge」のようなオプションを指定する必要があります。

TSU は我々が独自に設計した CPU ですから、当然ながら configure に「--target=tsu」などと指定するだけでは駄目です。もし本当に gcc を tsu に対応させ、それを世界に公開して、gcc の公式な配布の一部とするならば、configure のスクリプトを変更する必要があります。しかし、今回は TSU-GCC を開発しても自分たちで使用するだけですから、そのような用途のために用意された「local」という特殊なターゲットを指定しました。

gcc のプロセッサ依存部分は、原則としてすべて「config/ターゲット名」というディレクトリの下にあります。その中でも中心的なファイルが、「ターゲット名.md」という名前の Machine Description(md と略す)と 、「ターゲット名.h」という名前の Target Macros(tm と略す)です。

とりあえず今はファイルの中身は空にしておき、configure だけを行うことにします。config ディレクトリの下に local という名前のディレクトリを mkdir し、その下で local.md と local.h の 2 ファイルを touch してから、トップに戻って「./configure --target=local-local」とすれば OK です。

md ファイルや tm ファイルの書き方については、付属の info ファイルに書かれています。emacs から「C-u M-x info」として gcc.info を指定すれば読めます。他にこれといった資料がないので、まずはこれを熟読することにしました。特に重要だったのは、「Passes」「RTL」「Machine Desc」「Target Macros」の 4 つの章です。

なお md や tm の具体例としては、i386 や sparc のような高機能で複雑な CPU より、spur や m68k などのシンプルでわかりやすいプロセッサのほうが、ずっと参考になりました。ただ実際には、とにかく資料が不足していたので、ほとんどすべての CPU 用のファイルを多少なりとも参照する必要がありました。

6.2 第 2 週

まる 1 週間ほど info やいろいろな CPU の依存ファイルを読みあさって、ようやく md や tm の書き方がわかってきました。実際のところ、プロセッサ依存情報の書き方にもいろいろな流儀があり、どの方法を使うかは主として記述者の好みによるようです。以下に述べる方法も、いくつかの選択の中から僕が自分で選んだ方法にすぎないので、他にもいろいろなやり方が可能だったと思います。

まずは TSU の持つインストラクション・セットを md ファイルに記述していきます。「define_insn」という procedure を使って、各命令の機能を RTL という gcc が用いる中間言語で説明するのです。たとえば、TSU の「and」という命令は次のように定義されます。


(define_insn "tsu_and"
  [(set (match_operand:QI 0 "register_operand" "=r")
        (and:QI (match_operand:QI 1 "register_operand" "%r")
                (match_operand:QI 2 "register_operand" "r")))]
  ""
  "and\\t%1, %2, %0")

最初の引数 "tsu_and" は、この命令につけた名前です。先頭に「tsu_」をつけるのは、後で出てくる Standard Names との重複を回避するためです。

第 2 引数がこの命令の機能を記述する RTL です。簡単に述べると、この命令は「第 1 オペランドのレジスタと第 2 オペランドのレジスタのビット and をとり、その結果を第 0 オペランドのレジスタに書き込む」という意味になります。

何回かあらわれる「:QI」というのは、この命令の「モード」を指定するものです。QI は Quarter Integer の略で、1 ワードが 1 バイトに等しいモードをあらわします。ここでいう 1 バイトは 8 ビットではなく、TSU でアドレス可能な最小単位のことなので、実際には 16 ビットとなります。

"=r" "%r" "r" などと書かれている部分は constraint というもので、オペランドの種類を選択します。r はレジスタ、m はメモリ参照、i は即値などいろいろな constraint が用意されており、tm ファイルの中である程度は独自の constraint を定義することもできます。「,」で区切って複数の constraint を記述することも可能です。

= や % などの記号は modifier と呼ばれるもので、オペランドの性質を規定するものです。= はオペランドが write only であることをあらわします。ちなみに、+ という modifier は both read and write を意味します。= も + もついていないオペランドは read only とみなされます。% はそのオペランドと次のオペランドが可換であるという意味です。他にもいくつかの modifier があります。

"register_operand" となっている箇所は predicate と呼ばれ、オペランドの種類を限定するものです。register_operand の他に memory_operand や immediate_operand、あるいは general_operand などがあります。constraint との違いがわかりにくいのですが、predicate は主として pattern matching の判定基準に、constraint はより細かい種々の判断に利用されるものです。

このあたりは gcc の効率至上主義の影響もあって、あまり美しいとはいえない仕様になっています。実際に md ファイルを書いていても、まったく問題ないように思える記述がコンパイラを crash させる原因になったり、オペランドの種類を思ったように選んでくれなかったり、いろいろと悩んだところです。

第 3 引数は、その命令を利用するための条件を記述するフィールドです。TSU の and 命令は常に利用可能なので、空文字列にしておきます。

第 4 引数はアセンブリ出力の際に利用される、命令のテンプレートです。RTL パターンにマッチしたオペランドは、「% オペランド番号」のように参照できます。

この and 命令は特に簡単な例ですが、他の命令も基本的には同様な方法で記述していきました。

6.3 第 3 週

今週も md ファイルの続きを書きます。基本的な算術演算命令は先週述べた方法で記述できるのですが、いくつかの命令はそう簡単にはいきません。

まず最初に悩んだのが、ソースとデスティネーションが重複している命令です。たとえば「gethh1」という TSU の命令は、第 1 オペランドの上位 8 ビットと、第 2 オペランドの上位 8 ビットを順に連結して 16 ビットの語を作り、第 1 オペランドに書き込むという命令です。このような破壊的命令は i386 などにも多いので i386 の md ファイルを見たところ、constraint にオペランド番号を書くことにより、そのオペランドが別のオペランドと同一であることを指定できるようです。よく読んだらこのことは info にもちゃんと書いてあり、gethh1 の場合は次のように記述できました。


(define_insn "tsu_gethh1"
  [(set (match_operand:QI 0 "register_operand" "=r")
        (ior:QI (and:QI (match_operand:QI 1 "register_operand" "0")
                        (const_int 65280))
                (lshiftrt:QI (match_operand:QI 2 "register_operand" "r")
                             (const_int 8))))]
  ""
  "gethh1\\t%1, %2")

次に困ったのは、delayed execution の扱いです。TSU はメモリアクセス命令が 1 つ、分岐命令が 3 つの delay slot を持っています。gcc にはそのような delayed execution をサポートする仕組みがあらかじめ備わっているので、基本的にはそれを利用しました。

しかし、TSU においてメモリからレジスタへデータをロードする場合、アドレスを指定する「addr」という命令と、レジスタへの読み込みを行う「load」という 2 つの命令の間に delay slot が必要なので、gcc が提供する枠組みでは処理しきれません。しかたがないので gcc が出力するアセンブリ・コードを、独自の「ポストプロセッサ」に通してからアセンブルすることにしました。

また、gcc がもともとサポートしている CPU の中に delay slot を 2 つ以上持つプロセッサがなかったためか、delayed branch の scheduling に大きなバグがありました。自分であまりうまく直せなかったので、bug-gcc@prep.ai.mit.edu にバグ報告をしたところ、実に素早くパッチを作ってくれて非常に助かりました。

それでもまだ問題が残っていました。3 つある delay slot を埋めるのに「長さ 1 の命令が 3 つ」という指定しかできず、長さが 2 以上の命令は当てはまらないのです。このため、人間が考えれば意味のある命令が入れられる delay slot も、gcc の出力では nop になってしまうという現象が目立ちました。

できれば自分で gcc 本体を改良して解決したかったのですが、もともとコンパイラ担当ではないこともあって、あまり多くの時間をさくことができず、結局この問題は未解決のままとなりました。このことは後にレイトレのプログラムをコンパイルしたとき、コードサイズが大きすぎてメモリが不足する問題の一因となります。

一番苦労したのが比較演算と条件分岐の問題です。TSU にはオーバーフロー・フラグがないので、signed int の比較が単純にはできません。gcc は 1 種類の比較命令と複数の分岐命令を想定しているので、そうでないプロセッサは何らかの工夫をする必要があります。TSU 以外にも RISC CPU や DSP には実際にそういうプロセッサが存在するので、info にもいくつかの解決法が紹介されていました。いろいろな試行錯誤の末、結局 TSU-GCC では次の分岐命令を覗き見して、signed か unsigned か調べるという方法をとりました。

もう一つ問題だったのはジャンプの距離です。TSU は前後 128 ワード未満の領域には 1 命令でジャンプすることができますが、それ以上遠くへジャンプするには 3 命令が必要になります。普通この種の問題はアセンブラが処理するのですが、TSU アセンブラはただでさえ開発が遅れていたため、これ以上多くの機能を要求するのは困難な状況でした。gcc にもこのような状況のために用意された機能があったのですが、種々の事情でうまく利用することができず、これも結局はポストプロセッサを作って対処することになりました。このポストプロセスによる書きかえでレジスタが一つ必要がなるので、比較演算では常に一つのレジスタを破壊することにしました。

最終的に、比較命令は次のように記述することができました。


(define_insn "tsu_cmpu"
  [(set (cc0)
        (compare (match_operand:QI 0 "register_operand" "r")
                 (match_operand:QI 1 "register_operand" "r")))
   (clobber (match_scratch:QI 2 "=&r"))]
  "! tsu_next_cc0_user_is_signed (insn)"
  "*
{
  return (\"\\t\\t; begin cmpu\;\"
          \"cmp\\t%0, %1\;\"
          \"\\t\\t; end cmpu\;\"
          \"\\t\\t; %2 is free\");
}")

(define_insn "tsu_cmps"
  [(set (cc0)
        (compare (match_operand:QI 0 "register_operand" "r")
                 (match_operand:QI 1 "register_operand" "r")))
   (clobber (match_scratch:QI 2 "=&r"))]
  "tsu_next_cc0_user_is_signed (insn)"
  "*
{
  return (\"\\t\\t; begin cmps\;\"
          \"xor\\t%0, %1, r15\;\"
          \"add\\tr15, r15, r15\;\"
          \"jc\\t1f\;\"
          \"nop\;\"
          \"nop\;\"
          \"cmp\\t%1, %0\;\"
          \"cmp\\t%0, %1\\n\"
          \"1:\;\"
          \"\\t\\t; end cmps\;\"
          \"\\t\\t; %2 is free\");
}"
  [(set_attr "length" "7")])

ちなみに、そのポストプロセッサはアセンブラ担当者がアセンブラ完成後に作ってくれたのですが、アセンブラを呼び出してはエラーメッセージを見て該当箇所を修正する、という処理を繰り返す仕組みだったので、処理が非常に遅くなってしましました。レイトレプログラムの場合、gcc によるコンパイル自体は 5 秒で終わるのに、この処理に 5 分以上かかるというありさまでした(笑)。

6.4 第 4 週

先週はインストラクション・セットの記述をほぼ終えたので、今週は gcc が要求するいくつかの標準的な処理の実現方法を、define_expand という procedure で記述します。そのような標準的な処理には standard name と呼ばれる名前がついており、それぞれの名前に応じて define_expand で処理を実現する命令列を生成するのです。

たとえば、Quarter Interger のビット and を行う「andqi」は次のように書きます。


(define_expand "andqi3"
  [(set (match_operand:QI 0 "register_operand" "=r")
        (and:QI (match_operand:QI 1 "register_operand" "%r")
                (match_operand:QI 2 "register_operand" "r")))]
  ""
  "
{
  emit_insn (gen_tsu_and (operands [0],
                          operands [1],
                          operands [2]));
  DONE;
}")

この場合はちょうど対応する一つのインストラクションがあるので、記述が非常に簡単です。define_insn で定義したインストラクション tsu_and を利用するため、名前の前に gen_ をつけた関数で対応する RTL 式(RTX と呼ばれる)を生成し、それを emit_insn で出力します。

もっとも常にこのように簡単な記述ができるわけではありません。たとえば、整数の加算をあらわす「addqi」の場合、素朴な記述だとレジスタとレジスタの和しかできませんが、TSU には普通の CPU と同様に 1 を足す命令があるので、足す数が小さな定数の場合はそれを利用するように記述するべきです。また、整数の乗算「mulqi」などの高度な処理は、複数の命令を組み合わせないと実現できません。さらに複雑な命令は、そのまま展開するとコードサイズが大きくなってしまうので、サブルーチン呼び出しとして実装します。

define_expand の記述を行う際、一時的に値をしまうテンポラリ・レジスタが必要になったり、特定のレジスタや定数を表現したくなったりする場合があります。そのようなときは、gen_reg_rtx() や gen_rtx() などの標準で用意されている関数を呼び出して、求める RTX を生成します。

ほとんどの整数演算は以上のような方法で定義することができます。普通の CPU ならば浮動小数演算も同様に書けるのですが、TSU はハードウェアによる浮動小数演算命令を持たないため、いろいろと面倒な問題があるので今は後回しとします。


前のページ 次のページ 目次