C++ でのビルド時間を短縮するいくつかの方法

ある程度大きな C++ のコードを書いたことがある人なら大抵はこの問題について考えますよね。まして Boost なんて使っていた日には「コンパイル時間が Boost される」とか言われる訳です。
コンパイル時間を活用してコーヒー入れたりトイレ行ったりブラウジングしたり Twitter に「リビルドなう」とか Post したりといった素晴らしい方法もありますが、ここではビルド時間を短縮する方法を考えていきます。

事前知識

多分どこでも言われてることだと思いますし、結構適当に書いてるので読み飛ばしてもいいと思います。

コンパイルが遅い原因

C++コンパイル時間の多くは、プリプロセッサにあります。プリプロセス時に行われる include やマクロの展開でかなり多くの時間を取っています。
きっと Boost.PP なんて使っていた日には「コンパイル時間が Booooooooooooooooost される」とか言われて(ry
もちろんこれが遅い原因の全てではないのですけど、プリプロセスの時間を短縮することで、結構な改善ができます。

時間短縮の方向性

例えば 1 ファイル 100 秒掛かる cpp ファイルのみで構成されていた場合、ちょっと書き換えるたびに 100 秒待たされることになります。
しかし、これを 1 ファイル 2 秒掛かる cpp ファイル 100 個に分割した場合、全体をリビルドした際のコンパイル時間は 200 秒になりますが、1 ファイルを変更した場合のコンパイル時間は 2 秒になります。
大抵は C++ コードの一部だけを書き換えて再コンパイルするため、全体ビルドの時間が多少でかくなったとしても、単体でのコンパイル時間が早くなれば、全体としての時間効率としては上がる可能性が高いです。

ヘッダの依存性

一部のヘッダファイルだけを書き換えて再コンパイルした場合、少し書き換えただけですごい量のファイルがコンパイルされていく可能性があります。これは各 cpp ファイルが多くのヘッダファイルに依存していて、そのそれぞれのヘッダファイルがまた多くのヘッダファイルに依存して、という状況なので、その中のどれか 1 つのヘッダが変更された場合は再コンパイルし直さないといけないのです。

一般的な解決策

多分どこでも言われてる方法だと思いますし、結構適当に書いてるので読み飛ばしてもいいと思います。

Pimpl イディオムの利用

Pimpl イディオムを使うことで、ヘッダファイル内での依存するファイルを減らすことができるので、ヘッダファイル悲しみの連鎖を断ち切ることができます。詳しくはググれ
そのため「1 つのファイルを書き換えたらリビルドするのと変わらないぐらいの時間が掛かった」という状況を防ぐことができます。

プリコンパイル済みヘッダの利用

大抵の環境では、恐らく「プリコンパイル済みヘッダ」というのが用意されていると思います。これは事前にプリプロセスを行っておいたものを用意しておくというもので、プリプロセスにおける時間を短縮してくれます。
標準のヘッダや Boost のヘッダなんかをプリコンパイル済みヘッダに入れておけば、各 cpp ファイルで Boost をプリプロセスする必要が無くなるので、かなりの時間短縮が期待できます。
ただし、プリコンパイル済みヘッダに含まれているヘッダを少しでも変更した場合、それに依存している cpp ファイルが全部コンパイルし直されるので、時間を掛ければ掛けるほどお金が貰えるようなお仕事をしている場合を除いて、頻繁に書き換えられるヘッダをプリコンパイル済みヘッダに入れるのは止めましょう。

ハードウェア性能を上げる

もっといい PC を使ったり、PC を 10 台ぐらい買ってきてそいつらに分散コンパイルさせたりといった方法です。
ヘタな方法使ってコードの構造を崩すより、財力に余裕があるならこれでいい気がしますね。

一般的でない方法

多分まっとうな職場でやったら怒られますけど、オススメな方法です。

cpp ファイルは 1 個だけ

よく考えてみて下さい、コンパイル時間の多くはプリプロセスの時間にあります。コンパイルするファイルの数が多くなればなるほどプリプロセスに取られる時間は多くなります。
ここでコンパイル時間=プリプロセス時間ということにしてみましょう。この場合、cpp ファイルが 10 個あれば 10 回分のプリプロセスの時間が掛かります。うまく Pimpl を駆使して cpp ファイルのコンパイルする数を減らしたとしても、平均して 1 になることはありえません。しかし cpp ファイルを 1 個にしてしまえば、常にプリプロセス 1 回分の時間しか掛からないため、常に最小です。
実際はコンパイル時間が多少ありますし、プリプロセスの量も変わってくるため、cpp ファイルが多い場合の 1 ファイルのコンパイルの方が早いのですが、よっぽどプリプロセスする量が異なってない限りはそこまで大した差はありません。
それにですね、今時の普通の C++ プログラマはテンプレートをバリバリ使ってたり、Boost みたく実装を全部ヘッダに書くのが当たり前になっているので、Pimpl なんて使っていると非常に開発効率が悪いです。
cpp ファイル 1 個だけにして、あとはヘッダに全部書いていくという手法は、普通の C++ プログラマにとっては非常に開発効率が良くて、テンプレートとの相性も良く、そして全体としてのコンパイル時間も短くなります。非常に効率的です。


実際に BREW のゲームを作っていたときはこの方法でやっていたのですけれども、5 万行程度あったコードを VS2008 コンパイルしても 4 秒程度でした。C++ のコードでコンパイルするたびに 4 秒待つ程度なら余裕ですよね?

既存のプロジェクトへの適用

既に cpp ファイルが大量にあるようなプロジェクトでも、コンパイルするファイルを 1 個にすることができます。
まず、それらの cpp ファイルを全て「コンパイルしない」設定にしておきます。そしてそれらの cpp ファイルを全て include した cpp ファイルを作って、プロジェクトに追加します。この cpp ファイルをコンパイルすれば、他の cpp ファイルが全て include されるので、これで無事ビルドができます。
問題としては、同名の static な関数やデータがある場合に名前が被ってしまうことなのですが、これは手作業で直すしか無いと思います。


実際に自分は、cpp ファイルが 41 個あるプロジェクトを引き継いで開発していたのですけれども、自分は普通の C++ プログラマなのでヘッダに実装を書いていくわけです。気がついたらヘッダファイルが 133 個になっていて、41 ファイルのリビルドで 190 秒掛かっていました。1 ファイルあたり 4.6 秒程度ですね。しかも 1 つ書き換えただけで 30 個ぐらいのファイルがビルドし直されてめちゃめちゃ時間が掛かっていたという状況に。
ちょっと耐えられなくなってきたので、上記の方法みたいに cpp ファイルを 1 個用意して、他の cpp ファイルを include する方法を試してみたところ、全体で 7 万行程度のコードが 15 秒でビルドが終わるようになりました。これは GCC ベースのコンパイラです。まあギリギリ待てる範囲かなーとは思うのですけど、微妙かもしれません。


また、どちらの例も Boost は使っていないので、もしかしたら 1 ファイルでもコンパイル時間が Boost するかもしれないのですけれども多分それは Boost のファイルだけをプリコンパイル済みヘッダにしておいて、それを使うようにすればある程度は解決できるかもしれません。
ただユーザコードで Boost.PP を使っていたりなんかした場合はどうなっても知らないです。


あと、1 個の cpp ファイルにするとは書いていますが、ある程度少なければ 1 個じゃなくてもいいです。例えばあるファイルだけコンパイル時の設定が違うのであれば、それはどうしようもないので別ファイルにするしかありません。


ところで、これはたかだか数万行程度のプロジェクトだからいいかもしれませんが、でかいプロジェクトだともっと行数が増える可能性があります。
しかしよく考えると、そんなでかいプロジェクトでも、内部でいくつかのプロジェクトに分けてそれぞれやっているはずで、それぞれのプロジェクトでこの方法を使えば、よっぽどオールインワンなプロジェクトでない限りはこの方法で平気なんじゃないかと思います。

デメリット
  • ヘッダファイルのみで書こうとすると、ヘッダが循環参照したりして悩むことがあります。そういう場合はテンプレートを使ってヘッダに依存しないようにするなり、cpp ファイルに書き出して、それらを include するファイルを作って置きましょう。
  • ヘッダのみで作る普通の C++ プログラマと、cpp ファイルを作りまくる普通じゃない C++ プログラマの間でケンカになります。どうにか折り合いを付けて下さい。大抵負けます。
  • 上司に怒られます。

まとめ

もっと cpp ファイル 1 個の手法が流行ればいいのにね!