ビルド時間に気を配ってみる

はじめに

こんにちは、エンジニアの古矢です。

私の前回の投稿から5年経つようです。
エンジニア歴が長くなり、プログラミングに際して考えることも増えました。
読みやすいソースや、ランタイムでの実行速度などはよく聞く話です。
今回は少し視点を変えて、ビルドの効率化について書いてみようと思います。

想定ケース

実際に現場であったことをもとに、以下のようなケースを想定します。

  • プログラミング言語はC++
  • 実装されるアクターに対応するActorIdという列挙型が定義される
  • アクターの基底クラスで、自身のActorIdを返す関数が用意されている
  • ActorIdは、設定ファイルから社内ツールによって自動生成される
  • 設定ファイルはエンジニア以外でも変更でき、頻繁に更新される
// ActorId.h
// 設定ファイルから自動生成される

enum class ActorId : uint32_t
{
    Player,
    Npc01,
    Npc02
};
// Actor.h
// アクターの基底。全てのアクターはこれを継承する

#include "ActorId.h"

class Actor
{
public:
    // 自身のActorIdを返す
    ActorId GetActorId() const;
};

この設計では、列挙型が更新されるたびに全アクターが再コンパイルを免れません。
そして更新が頻繁なため、通常ビルドも頻繁にほぼフルビルドになってしまいます。

不要なインクルードを減らす

動作が変わらないところで再コンパイルしていては作業効率が落ちます。
前方宣言を使って不要なインクルード避けます。
余談ですが、列挙型に前方宣言が使えることは1年ほど前まで知りませんでした。

// ActorIdDeclaration.h
// 前方宣言のみ行う

enum class ActorId : uint32_t;
// ActorIdDefinition.h
// 設定ファイルから自動生成される。
// 必要なソースからのみインクルードする

enum class ActorId : uint32_t
{
    Player,
    Npc01,
    Npc02
};
// Actor.h
// アクターの基底。全てのアクターはこれを継承する

// 前方宣言だけインクルード
#include "ActorIdDeclaration.h"

class Actor
{
public:
    // 自身のActorIdを返す
    ActorId GetActorId() const;
};

これでActorIdの数値が必要なソース以外は頻繁な更新から解放されます。
しかし、ここで新たな問題が浮き彫りになってきました。

分散ビルドのボトルネック

社内のPCで余っている処理能力を融通しあう、分散ビルドというものがあります。
分散ビルドではソースファイル単位でコンパイルを手分けされているようです。

再コンパイル対象が減ったことで、単体でコンパイル時間が長いソースが取り残されるようになりました。

このファイルがActorIdを必要としており、ボトルネックになっていました。

ボトルネックを逃がす

ボトルネックとなるソースは以下のような構成でした。

// Slime.h
// マイナーチェンジ版の挙動をまとめて実装しているアクタークラス

#include "Actor.h"

class Slime : public Actor
{
public:
    // 更新
    void Update();
};
// Slime.cpp
// ActorIdを使うのでActorIdDefinition.hをインクルードする

#include "Slime.h"
#include "ActorIdDefinition.h"

void Slime::Update()
{
    // 少しだけ特殊処理
    switch (GetActorId())
    {
    case ActorId::SlimeRed:
        /* Redの特殊処理 */
        break;

    case ActorId::SlimeGreen:
        /* Greenの特殊処理 */
        break;

    case ActorId::SlimeBlue:
        /* Blueの特殊処理 */
        break;

    default:
        break;
    }
}

// 数千行あるファイルであり、コンパイルに時間がかかる

ボトルネックとなるソースでも、ActorIdが全て必要なわけではありません。
必要な判定だけ別ソースに分離します。

// Slime.h
// マイナーチェンジ版の挙動をまとめて実装しているアクタークラス

#include "Actor.h"

class Slime : public Actor
{
public:
    // 更新
    void Update();

public:
    // 種類を自分専用に定義
    enum class Variation : uint32_t
    {
        Red,
        Green,
        Blue
    };

    // ActorIdとVariationの相互変換を用意
    static Variation ToVariation(ActorId actorId);
    static ActorId ToActorId(Variation variation);
};
// Slime.cpp
// このファイル中でActorIdを使わなくなったので
// ActorIdDefinition.hのインクルードは不要になった

#include "Slime.h"

void Slime::Update()
{
    // 少しだけ特殊処理
    switch (ToVariation(GetActorId()))
    {
    case Variation::Red:
        /* Redの特殊処理 */
        break;

    case Variation::Green:
        /* Greenの特殊処理 */
        break;

    case Variation::Blue:
        /* Blueの特殊処理 */
        break;

    default:
        break;
    }
}

// 数千行あるファイルであり、コンパイルに時間がかかる
// SlimeVariation.cpp
// ActorIdに依存する関数だけこちらで実装する。
// コード量が少ないため、再コンパイルが発生しても短時間で済む

#include "Slime.h"
#include "ActorIdDefinition.h"

Slime::Variation Slime::ToVariation(ActorId actorId)
{
    switch (actorId)
    {
    default: /* SlimeRed */
        return Variation::Red;

    case ActorId::SlimeGreen:
        return Variation::Green;

    case ActorId::SlimeBlue:
        return Variation::Blue;
    }
}

ActorId Slime::ToActorId(Variation variation)
{
    switch (variation)
    {
    default: /* Red */
        return ActorId::SlimeRed;

    case Variation::Green:
        return ActorId::SlimeGreen;

    case Variation::Blue:
        return ActorId::SlimeBlue;
    }
}

ActorId更新時の再コンパイル対象が数千行から数十行に減りました。

ビルド効率化の恩恵

これらの対応により、当時15分弱かかっていたビルドを3分以上短縮できました。
1回あたりの恩恵は大きくありませんが、人数や期間が大きいほど恩恵は大きくなります。
忙しいと細かいことまで気が回らないことも少なくありません。
そんな時こそ作業の効率化のために時間を割いてみるのはどうでしょうか。