Ninjaを試した

Google Chromeの開発者が新たに作った、GNU makeなどと同じ類のビルドシステム。どんな感じなのだろうと興味は持っていたが、先日ドキュメントを読んだ時にすぐに試せなかった。今回、休日を利用してMac OS X上でコンパイル、実際に使ってみた。
今のところ個人的にNinjaに注目しているのは「ビルドにかかる時間の短縮化」と「Makefile(的なファイル)の書きやすさ」なので、今回はその二つについて書く。

ビルドにかかる時間の短縮化

比較対象はautomakeなどから作ったMakefileを用いたビルド、Ninjaのbuild.ninjaでのビルド、build.ninjaをベースに書き起こしたbuild.makeでのビルド。簡単に今回の結果から書くと、automakeなどで自動生成したMakefileよりはビルド時間が縮まったが、他はほぼ同等。ファイル数がもっと多くないとNinjaの威力を発揮させられないのかも。
ビルドしたのは https://github.com/iwadon/junk (以下junkと書く)で、timeコマンドを使ってビルド開始からcheckターゲット実行の終了までを測った。最初のビルドではRubyでのテストも実行されているけどそんなには影響ないみたい。計測中の様子はこんな感じ:

macbook:~/src/junk/_build_% time make -j3 -s check                                                               (git)-[master]
../channel.cpp:37: warning: unused parameter ‘velocity’
../s2.cpp:43: warning: unused parameter ‘app’
../s2.cpp:140: warning: unused parameter ‘app’
/usr/bin/ranlib: file: libsequencer.a(libsequencer_a-filter.o) has no symbols
ranlib: file: libsequencer.a(libsequencer_a-filter.o) has no symbols
................................................................................................


OK (96 tests)


PASS: all_tests
Loaded suite ../peg_test
Started
..
Finished in 0.00319 seconds.

2 tests, 66 assertions, 0 failures, 0 errors
PASS: peg_test.sh
==================
All 2 tests passed
==================
make -j3 -s check  104.55s user 12.41s system 145% cpu 1:20.17 total
macbook:~/src/junk/_build_% make -s clean                                                                        (git)-[master]
macbook:~/src/junk/_build_% time make -j3 -s -f ../build.make check                                              (git)-[master]
../channel.cpp:37: warning: unused parameter ‘velocity’
/usr/bin/ranlib: file: libsequencer.a(filter.o) has no symbols
................................................................................................


OK (96 tests)


make -j3 -s -f ../build.make check  94.04s user 9.78s system 168% cpu 1:01.65 total
macbook:~/src/junk/_build_% rm -rf build                                                                         (git)-[master]
macbook:~/src/junk/_build_% time ../../ninja/ninja -f ../build.ninja check                                       (git)-[master]
[14/71] CXX build/oscillator.o
CXX build/channel.o
../channel.cpp:37: warning: unused parameter ‘velocity’

[32/71] CXX build/channel_test.o
AR build/libsequencer.a
/usr/bin/ranlib: file: build/libsequencer.a(filter.o) has no symbols

[70/71] RUN build/all_tests
RUN build/all_tests
................................................................................................


OK (96 tests)



[71/71] RUN build/all_tests
../../ninja/ninja -f ../build.ninja check  93.89s user 9.89s system 164% cpu 1:03.15 total
macbook:~/src/junk/_build_%                                                                                      (git)-[master]

Ninjaを作ろうとした動機として作者は「ビルドが始まるまでの時間の短縮化」を挙げていたので、もし肩書き通りの性能を持つならばビルドに必要なファイル数が多ければ多いほどNinja有利ということになるのだろうか。今回使ったjunk程度のファイル数ではあまり差は出なかったが、お仕事で使われているMakefileは結構ファイル数が凄いことになっているので、ぜひ試してみたい。少なくともGNU makeよりも遅いことはなかったということで良かった。
automakeで生成したMakefileを使ったビルドに時間がかかっている事自体特に感想はないが、素のMakefile(build.make)と10秒もさが付いたのにはちょっと驚いた。automakeで生成したMakefileには色々とシェルスクリプトが書かれてて、色々と便利なことになっているんだけど、ビルド時間の増大はMakefile.amの便利さとのトレードオフなのかもね。そういえば仕事ならともかく趣味では直接Makefile書かなくなってしまったなあ。
追記3にあるように、テストを実行する部分で時間がかかっている可能性がある。

Makefile(的なファイル)の書きやすさ

NinjaがNinja自身をビルドするためのbuild.ninjaを読みながら、junkのMakefile.amをもとにbuild.ninjaを描いてみた。Makefileの書式に慣れた身としては当初かなり違和感があったが、書き進めると意外と書きやすかった。気に入った点はファイル名のマッチングに頼らないルール付けや、依存ファイル周りの仕組み、各ターゲット毎のカスタマイズ方法、などなど。

変数

変数の定義あたりはGNU MakeのMakefileとあまり違いなく見える:

srcdir = ..
builddir = build
cc = gcc
cflags = -Wall -Wextra -O2 -g
cxx = g++
cxxflags = -Wall -Wextra -O2 -g
objc = gcc
objcflags = -Wall -Wextra -O2 -g
cppflags = -DHAVE_CONFIG_H -I. -I$srcdir -I/Users/don/local/include/SDL -I/Users/don/local/include
ldflags = -g -L/Users/don/local/lib
libs =

Ninjaのbuild.ninjaでは変数名を小文字で書かれてたので、junkのbuild.ninjaでもそれに従った。

ルール

例えばC++のソースファイルをコンパイルするルールはこんな感じに書く:

rule cxx
  depfile = $out.d
  command = $cxx -MMD -MF $out.d $cxxflags $cppflags -c $in -o $out
  description = CXX $out

cxxというのはルール名、depfileは依存関係が書かれたファイルの名前、commandは実行するコマンドラインdescriptionはビルド中に表示する説明書きのようなもの。$が付いたものは変数の参照で、その中でも$in$outGNU makeの$^$@にあたる。依存関係周りでgcc -MMから得られる情報そのまんま利用しているため、build.ninjaには依存関係を示すためのターゲットは書いていない。

ターゲット

junkに含まれるbenchmark_sampleというコマンドを生成するターゲットはこんな感じ:

build $builddir/s2.o: cxx $srcdir/s2.cpp
build $builddir/s2: link $builddir/s2.o $builddir/libsdlapp.a $builddir/liblogger.a
  ldflags = $ldflags -framework OpenGL
  libs = -lSDL -lSDL_image

行頭にbuildとあるのがターゲットを示し、ターゲット名はGNU Makeなどでもお馴染みの書き方。コロンを挟んだ次にあるcxxlinkは先ほど出てきたルール名で、これをターゲット毎に指定することでどのようにビルドするかを決める。このルールを明示する方法は結構好きだが、逆に各ターゲット毎に異なったコマンドラインを実行する為にはいちいちルールを書く必要がありそうなのかな?
3行目以降はGNU Makeなどであれば通常実際に実行するコマンドラインを書くところだが、Ninjaではターゲット毎に変数をカスタマイズするための記述をここで行う。Makefile.amを書く時と似た形で個別のLDFLAGSやLIBSを指定できたので、生のMakefileを書く時みたいにコマンドラインを全部書く必要が無いのは嬉しい。
ビルドしたテスト用プログラムの実行はこんな感じ:

rule run
  command = $in
  description = RUN $in

build check: run $builddir/all_tests

この例では1行実行するためのルールを用意している。まだ調査不足のため、2行以上のコマンドラインを実行する方法がわからない。そういうコトは個別にスクリプトとかで外出ししておけという方針だったりして?

まとまらないまとめ

触り始めたばかりであまり手応えを感じていないが、もうしばらく試してみたい気にはさせられた。ビルド時間もGNU Makeとそう変わらないみたいだし、build.ninjaは当初書式に面食らったくらいでわりかし書きやすそうだ。今後はもう少し規模の大きいプロジェクトで試していたい。
Ninjaのソースコードもまだ少ないので、今のうちに中身を読んでおきたいところ。仕事のプロジェクトでは依存関係の調査に結構時間がかかることや、GNU Makeからコマンドの実行に意外と時間がかかることなどを個人的に問題視しているので、そのあたりを調べたい。加えてWindowsCygwinでのコンパイルや、Borland C++などでのコンパイルが出来るかどうかにも興味がある。

追記

そういえばNinjaがデフォルトで-j3だったのでGNU Makeでもそれに合わせたんだけど、普段GNU Makeの時の-j1だとどうなるかを試した:

make -s check  96.43s user 10.62s system 96% cpu 1:51.08 total
make -s -f ../build.make check  86.51s user 8.75s system 94% cpu 1:41.25 total
../../ninja/ninja -j1 -f ../build.ninja check  86.30s user 8.72s system 95% cpu 1:39.14 total

なんか全体的に時間が短縮されてる。もしかしてMacBook(CPUは二つみえている)だからかな。ついでに-j2も:

make -s -j2 check  102.84s user 11.71s system 164% cpu 1:09.71 total
make -s -j2 -f ../build.make check  92.11s user 9.58s system 165% cpu 1:01.33 total
../../ninja/ninja -j2 -f ../build.ninja check  92.28s user 9.75s system 167% cpu 1:01.07 total

CPUが複数見えるのなら-j2の方が早く終わるもんだと思ってたけど、そう単純な話でもないのね。

追記2

テストを行わないallターゲットも試した:

make -s -j1 all  26.54s user 4.23s system 96% cpu 31.758 total
make -s -j1 -f ../build.make all  25.68s user 3.96s system 95% cpu 30.983 total
../../ninja/ninja -j1 -f ../build.ninja all  25.83s user 4.10s system 97% cpu 30.847 total
make -s -j2 all  27.71s user 4.63s system 166% cpu 19.370 total
make -s -j2 -f ../build.make all  26.88s user 4.31s system 174% cpu 17.855 total
../../ninja/ninja -j2 -f ../build.ninja all  26.89s user 4.44s system 161% cpu 19.421 total

これだけ見るとそんなにビルド時間に開きはない。automakeなどで生成したMakefileを使うと他との開きが大きくなるのは純粋なビルドじゃなくてテストを実行する部分に違いがあるのかな。

追記3

テストを実行しないで試した:

make -s -j3 ./all_tests  94.77s user 10.24s system 166% cpu 1:03.01 total
make -s -j3 -f ../build.make ./all_tests  93.92s user 9.71s system 171% cpu 1:00.58 total
../../ninja/ninja -f ../build.ninja build/all_tests  93.83s user 9.83s system 167% cpu 1:02.06 total

やはりそんなに差はない。automakeなどで生成したMakefileではテストを実行する部分で時間がかかっている可能性が高い。