GCC とメタプログラミング

GCC やっぱすごいなぁという話

まぁコード生成する類のものは全部メタプログラミングだと考えるとします。当時のkinaba先生世界観。例としては C のコードを cpp で C にするとか、 C のコードを gcc で asm にするとか autotools でほげほげとか。

それはともかく最近思いついた(全然コードを読まない)コードリーディング技法として make してみてその標準出力をゆっくり眺めるというものがあります。ファイルの依存関係がわかれば割と内容は予測がついたりします。あと GCC のコード眺める技法として gdb で適当に break しかけてバックトレースを見ると普通に追うよりラクというのがあるのですがそれは別の話。

でまぁ make の出力とか見てると GCC ってすごいメタプログラミングの宝庫だなぁと思ったのでした。特にフロントエンド側よりバックエンド側がにんともかんとも。いくつか目に入ったのを書いてみる。

マクロ

CPU ごとに吐くアセンブリを変えている部分を GCC のバックエンドというわけですが、主に target macro という C のヘッダファイルと、 machine description という S 式っぽい設定ファイルで記述されてるそうです。 target macro の方はその名の通り C のマクロが色々と定義されていて、わかりやすいところでは STACK_GROW_DOWNWARD とかそんなマクロを定義していくわけです。他だとアドレッシングに使えるレジスタかどうかを 0 1 で返すマクロとかそんな感じで、 C++ の policy とかを思い起こすような。よくわからんけどこういうの

#define REGNO_OK_FOR_BASE_P(REGNO)                                      \
  ((REGNO) <= STACK_POINTER_REGNUM                                      \
   || (REGNO) == ARG_POINTER_REGNUM                                     \
   || (REGNO) == FRAME_POINTER_REGNUM                                   \
   || (REGNO >= FIRST_REX_INT_REG                                       \
       && (REGNO) <= LAST_REX_INT_REG)                                  \
   || ((unsigned) reg_renumber[(REGNO)] >= FIRST_REX_INT_REG            \
       && (unsigned) reg_renumber[(REGNO)] <= LAST_REX_INT_REG)         \
   || (unsigned) reg_renumber[(REGNO)] <= STACK_POINTER_REGNUM)

あとは有名所かもですが include のテクニックでしょうか。 tree.def には

 DEFTREECODE (TREE_LIST, "tree_list", tcc_exceptional, 0)
 DEFTREECODE (TREE_VEC, "tree_vec", tcc_exceptional, 0)

とかそういうのが並んでいて、これをどうやって使うかというと前もって DEFTREECODE を define してから include することによって好きな部分を取り出せる、という。具体的には例えば getcheck.c とかだと、

 #define DEFTREECODE(SYM, NAME, TYPE, LEN) #SYM,
 
 static const char *const tree_codes[] = {
 #include "tree.def"
 #include "c-common.def"
 #include "gencheck.h"
 (char*) 0
 };

などと。

コード生成

さっきの gencheck.h ってファイルですが、オリジナルのソースツリーには無いファイルだったりします。どう作られるかというと、 gcc/configure.ac が言語ごとに $srcdir/$subdir/$subdir-tree.def というようなファイル (C++ なら gcc/cp/cp-tree.def) を検出して、その変数が AC_SUBST で gcc/Makefile.in の方に伝わって、 Makefile

s-gencheck : Makefile
        ltf="$(lang_tree_files)"; for f in $$ltf; do \
            echo "#include \"$$f\""; \
        done | sed 's|$(srcdir)/||' > tmp-gencheck.h
        $(SHELL) $(srcdir)/../move-if-change tmp-gencheck.h gencheck.h
        $(STAMP) s-gencheck

とかやってファイル一覧を適当に sed とかでいじりつつ作っている。

あと GCC でコード生成というと GGC とかが有名かと思います。 GGC は GCC 用の GC で、 gengtype* がメインのソースコードぽいです。 gengtype は C のコードを適当に字句構文解析して、そこらじゅうに見える GTY(()) とかいう謎のマクロをどうにかするらしいです。具体的には GTY(()) ってつけたグローバル変数GC root に置かれるとかそんなこんなぽい、けど調べてないので不明気味。

まぁそんなことよりたぶん machine description からのコード生成も大変なことになっいる。 machine description はさっきちょっと書いた通り S 式で記述されたものなのですが、具体的にはどういう命令があるか (define_insn) と、その命令群で GCC が要求する機能群をどう実現するか (define_expand) がメインだと思います。でまぁこの S 式は普通は実行時にパースしてどうこう…と考えるかと思うんですが、それだと実行時に遅くなってしまうのでコンパイル時に .md からファイルを生成する…ということになります。

で、作られるファイルの種類なんですが、 .c は当たり前で .h や .mk なんてものも .md から生成されます。

build/genmddeps ../../gcc/config/i386/i386.md > tmp-mddeps
/bin/sh ../../gcc/../move-if-change tmp-mddeps mddeps.mk

とか

build/genpreds -h ../../gcc/config/i386/i386.md > tmp-preds.h
/bin/sh ../../gcc/../move-if-change tmp-preds.h tm-preds.h

とかいった具合に。さらに .md 自体も生成されたりしています。この部分の経過はなかなか面白いです。

  • gcc/genconditions.c をコンパイルして実行ファイル genconditions を生成する。
  • genconditions は i386.md を入力として gencondmd.c を生成する。
  • gencondmd.c から実行ファイル gencondmd を生成する。
  • gencondmd を実行して insn-conditions.md を生成する。
  • でその後 insn-conditions.md は i386.md と一緒に他の色々なソースコードジェネレータに入力として使われる (GCC-4.2.2 で 12個所)。
  • 例えば genautomata.c からコンパイルされた genautomata は i386.md と insn-conditions.md を入力として insn-automata.c というファイルを出力する、など。

ちなみに .md から .md を生成する理由は gcc/genconditions.c によると、

/* In a machine description, all of the insn patterns - define_insn,
   define_expand, define_split, define_peephole, define_peephole2 -
   contain an optional C expression which makes the final decision
   about whether or not this pattern is usable.  That expression may
   turn out to be always false when the compiler is built.  If it is,
   most of the programs that generate code from the machine
   description can simply ignore the entire pattern.  */

とのこと。

途中でコード生成ツールとして活躍している autotools も GCCコンパイルされていて、今 GCCコンパイルしている GCC 自体も GCC で作られてるんだなぁ…とか考えるとなかなか感慨深いものがあります。

まとめ。

  • GCC は遅いとか言っても色々頑張ってるらしい。時間かかってる部分は最適化とかの部分が大きいんだろうけど。
  • メタプログラミングとかマクロとかをウリにする言語は GCC を 1 パスで効率的に作れると良いと思う。
  • はてなで (()) を出すのはむずかしい。
なにかあれば下記メールアドレスへ。
shinichiro.hamaji _at_ gmail.com
shinichiro.h