C++

C++のコンパイルとリンクがあまりに遅いのでカッとなって原因と対策を調べてみた

More than 3 years have passed since last update.


はじめに

美しく素晴らしいプログラミング言語が数多く存在する中、未だC++を使ってる業界は少なくなく、おそらく多くの人がそのコンパイル&リンクの遅さにイライラしていると思います。

ただでさえ遅いのに、C++様は僕達をあざ笑うかのように、リリースに近づけば近づくほどソースコード量の増大によってコンパイル&リンク時間も増えていきます。

プロジェクト末期は、溢れかえったタスクをどうにか消化するために少しでもコーディングに時間を割く必要があるのに・・・くそっorz


というわけでここではそんな不幸なプログラマを減らすためにも、僕の知る限りのコンパイル&リンク時間を減らす方法を書いてみようかと思います。


まぁ高性能PCや、インクレディビルドの購入が可能であればこんな苦労をしなくても済むかも知れませんが。。。


そもそも、コンパイル&リンクが遅くなると何がイケないの?


  • 単純に実装→確認に時間がかかると、実装完了までの時間も長くなる。

    ※そもそもソース変更でいちいち広範囲にビルドが走るということは、うまくモジュール化されてない可能性が高く、変更による影響範囲の確認が大変かと。


なんでコンパイル&リンクが遅くなるの?


  • コード変更時に影響するソースが多すぎる。うまくモジュール化されていない。

    →ヘッダに実装やインクルード書きすぎ。影響するコード多すぎ。

  • プリコンパイル済み/並列ビルドの設定が適切で無い

    →設定は簡単なのですが知らない人が割りと多かったり。。。

  • そもそもPCの性能が足りてない

    →CPU性能だけでなく、リンクに関してはHDD性能も影響してきます。

  • ソースコードが多すぎる。テンプレートとか複雑すぎる(細かい話)

    →プロジェクトの規模が小さい時は顕在化しません。規模が大きくなると大問題になります。気づいた時には割りと手遅れです。早期発見が大事。


でどうすりゃいいの?


ヘッダの依存を減らす。頻繁に変更の可能性があるコードはヘッダに書かない。

他言語のimportと違って#includeは指定したファイルを丸ごとそこへ挿入するのでインクルードが増えれば増えるほどビルド時間の増大に繋がります。

依存軽減はプロジェクトの初期からきちんと準備してないと、コード量が増えれば増えるほど対応が辛くなります・・・整理担当を決めたりチーム員に啓蒙するなど早めに手を打ちましょう。


  • ヘッダの依存を減らす方法


classD.h


#pragma once // 二重インクルード防止

#include "classA.h" // クラスAのヘッダ →継承してるので必要
// #include "classB.h" // クラスBのヘッダ →前方宣言できるのでcppにインクルードをかけば良い。
#include "classC.h" // クラスCのヘッダ →実態をメンバに持ってるので必要
// #include "classX.h" // そもそもヘッダで使ってない(もしcppで使ってる時は、cppに書くだけでOK)

class classB; // 定義がポインタであれば前方宣言だけでOK

class classD : public classA
{
public:
~
private:
classB* m_pClassB; // ポインタ
classC m_classC; // 実態

};



  • 継承している場合は仕方ないが、ポインタとしてクラスに持っておけば、前方宣言を書いて、cppでインクルードするだけで良い。

    インクルードを書く必要がなければ、そのヘッダが変更されてもビルドされるソースコードは減るのでコンパイル時間も減ります。


  • コレを応用した、Pimplイディオムという手法も有ります。使いようによってはコードがすっきりするかも。



モジュール化して依存を減らしたり、モジュールをdll化してリンク時間を短縮する(分け過ぎはNG)


  • モジュール毎にプロジェクトを分割すれば、自然と依存も切れるので結果プログラム変更時のビルド時間の短縮にもつながります。

  • また、静的リンク(staticLib)から動的リンク(dll)に変えることで、リンク時間の削減につながります。

    ※プログラム実行時にdllのロードに少し時間がかかるようになりますが、静的リンクに比べたら全然早かったです(特に理由は追求してないのですが。。。)ロード時間が気になるようだったら、開発時はdllにしておいて、リリース時に静的リンクに変えればOKです。





  • モジュール毎にDLL化していれば、プログラムを起動したまま動的ロードなんてことも可能になったり・・・


  • モジュールの粒度に関してはOSSなどを参考に・・・(僕も明確な答えを持ってないので。。。)



プリコンパイル済みヘッダ/並列ビルドを適切に設定する。


プリコンパイル済みヘッダとは?

コンパイルの最初にヘッダを全て解析し、解析済みのヘッダを使うことでヘッダ解析のコストを下げる手法。


ググれば詳しい方法が出てきますのでここでは簡単な説明で済ませます。


  1. 変更が少なく、いろんなところでインクルードされているようなヘッダ(windows.hとかstl関連とか)をまとめたヘッダファイルとヘッダファイルと同名のcppにインクルードを書いてプロジェクトに追加する。

  2. プロジェクトのすべてのソースファイルの先頭で、先ほど作成したヘッダファイルをインクルードする。

  3. プリコンパイルヘッダと同名のcppのプロパティ→構成プロパティ→C/C++→プリコンパイル済みヘッダーの作成/使用 で、 「プリコンパイル済みヘッダーを作成する」 に設定する。

  4. プロジェクトのプロパティ→構成プロパティ→C/C++→プリコンパイル済みヘッダーの作成/使用で、 「プリコンパイル済みヘッダーを使用する」 に設定して、 「ファイルを使用して・・・」 にプリコンパイルヘッダを指定する。

試しにビルドしてみて、設定前より明らかにビルドの速度がUPしていれば成功なはず。(適当すぎ)


並列ビルドとは?


  • ネットワークを使った並列ビルドの仕組みとしては、インクレディビルドが有名ですが、VisualStudioのみでもローカルPCののみですが並列ビルドが可能です。

    その方法とは・・・




    プロジェクトのプロパティから「構成プロパティ」→「C/C++」→「コマンドライン」の追加オプションに /MP を追加するだけ!簡単!

コレもビルドしてみて、タスクマネージャーのCPU使用率を開きながら、ビルド中に複数のCPUコアが使われてるっぽくてかつビルドが爆速になれば成功\(^o^)/(適当すぎ)


PC性能に関して


  • CPU性能を上げる

    → 主にビルド速度に影響します。クロック数が向上すればビルド速度もUPしますが、並列ビルド(/MP)であればコア数が多ければ多いほどビルド速度は向上します。

  • メモリを増やす

    → ビルド時のHDDスワップが減ることでビルド(リンクも?)速度が改善されます。

  • HDD→SSDに変える

    → HDDをSSDに変えることで、I/O速度が向上しリンクが早くなります。


ソースコードがデカイくて複雑


  • プリプロセスに時間がかかったり、ソースコードが複雑だとビルド時間に影響し、ビルド後のOBJファイルのサイズがデカイとリンクに影響します。




  • コードが複雑だったりコード量が多い(1ソース1万行とか)ソースは糞コードパラダイスな事が多いので、コードの複雑さを図るための手法C/C++のメトリクス集計(CCCC)が行えるツールなど活用し可視化して、極端に複雑で長いコードを早期発見し可能な限り駆逐しましょう。

    ※パラダイス発見の際は、探偵ナイトスクープにはがきにてご応募ください。

コレも依存と一緒で、コード量が増えれば増えるほど対応が辛くなります・・・整理担当を決めたりチーム員に啓蒙するなり始めから手を打っておきましょう。


複雑なテンプレート


  • STL\boostを使い始めただけでビルド時間が増えたと感じた経験があるはず。

  • プリプロセスで行われる テンプレートの解析はビルド時間に、 テンプレートの展開によるコードサイズの増加はリンク時間 に多大な影響をおよぼすので、テンプレートは用法用量を守って正しく使いましょう。

    →そういったプリプロセスが重いものはプリコンパイル済みヘッダに入れておけばある程度は改善される気がする?(特に根拠無し)

    →最近のコンパイラは賢いので、ある程度であればうまい具合にまとめてくれるかも?(特に根拠無し)




  • テンプレートを覚えたての時は、美しい(?)コードを書くためにいろいろ駆使したくなる時期は誰しもあると思いますが、人に迷惑がかかる前に自力で目覚めましょう。

    ※僕は先輩が迷惑かけてたのを見て目覚めました( ´∀`)


大量の強制inline


  • 強制inlineも割りと危険。処理速度を上げることだけが最適化と思い込んでいる人がかなりの数存在するため、すべての関数を強制inline化し、気づいてみたらOBJサイズが大変なことになり、ビルド時間やリンク時間に影響がでます。できるだけinline化は僕らより何倍も賢いコンパイラさんに任せましょう。


馬鹿でかいstatic領域がいっぱい(そんなに影響無いかも?)


  • ソースに馬鹿でかいstaticな配列が入ってたりしてもOBJのサイズがでかくなります。強制inlineおじさんや過激なtempleteヤンキーが居ないのにもかかわらず、なぜかリンク時間が多めな場合は、頑固なstaticおじいちゃんが隠れているかも知れません。見つけたら躊躇せず抹殺しましょう。(動的ヒープに変えましょう)


その他


まとめ


  • コードサイズが少なければコンパイルも早いよ。

  • 依存が少なければ変更してもビルドされるコードは少なくて済むよ。

  • コンパイラさんがビルド時間短縮のために頑張ってくれてるのでちゃんと受け止めよう。

  • C++は罠が多いのでちゃんと調べて特性をきっちり把握しておこうね。

  • やっぱLLVM/Clangでいいんじゃない?(ぇ



    以上、割りと感覚で書いちゃってる部分もあるので間違いだらけかもしれませんが。。。