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
と$out
はGNU 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などでもお馴染みの書き方。コロンを挟んだ次にあるcxx
やlink
は先ほど出てきたルール名で、これをターゲット毎に指定することでどのようにビルドするかを決める。このルールを明示する方法は結構好きだが、逆に各ターゲット毎に異なったコマンドラインを実行する為にはいちいちルールを書く必要がありそうなのかな?
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からコマンドの実行に意外と時間がかかることなどを個人的に問題視しているので、そのあたりを調べたい。加えてWindowsのCygwinでのコンパイルや、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ではテストを実行する部分で時間がかかっている可能性が高い。