41
36

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

CMakeAdvent Calendar 2014

Day 21

CMake: プリコンパイル済みヘッダーの作成と利用

Posted at

はじめに

みなさん、こんにちは。今回は、CMake でのプリコンパイル済みヘッダーの作成と利用方法について書いていきます。

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

C++ において、Boost の使用やテンプレートメタプログラミングは必須といっても過言ではありません。しかしこれらを駆使すると、ビルド時間がすぐに Boooooooooooooooost してしまいます。ビルド時間の増大は開発速度の低下を招き、プログラマの精神的負担にもなります。そのため、どうにかして高速化しなければなりません。高速化の手法にはいくつかありますが、機械的にできるものとしてプリコンパイル済みヘッダーの使用が挙げられます。プリコンパイル済みヘッダーとは、よく使うヘッダーをあらかじめコンパイルしておくことにより、そのヘッダーを使うコードのコンパイル速度を向上させることができるものです。しかし、プリコンパイル済みヘッダーは各ベンダーが独自に実装しており、標準的に定められているわけではありません。このため、プリコンパイル済みヘッダーの作成および使用には各ベンダーごとに違う手順を踏まなければならず、とても煩雑なものになっています。

ビルド高速化モジュール cotire

しかし、cotire というビルドの高速化を目的とした CMake モジュールを利用することにより、非常に簡単な手順でプリコンパイル済みヘッダーの作成と使用を行うことができます。例えば、次のプロジェクトがあったとします。

project-root/
├── CMakeLists.txt
└── main.cpp
CMakeLists.txt
cmake_minimum_required(VERSION 3.0.2)

find_package(Boost REQUIRED system thread)
add_compile_options(-std=gnu++1y)
include_directories(${Boost_INCLUDE_DIRS})

include_directories(${PROJECT_SOURCE_DIR})

add_executable(bin main.cpp)
target_link_libraries(bin ${Boost_LIBRARIES})
src.hpp
#include <iostream>
#include <boost/asio.hpp>

int main() {
    namespace asio = boost::asio;
    using asio::ip::tcp;
    
    tcp::iostream tcp_stream("www.boost.org","http");
    tcp_stream
        << "GET / HTTP/1.0\r\n"
        << "Host: www.boost.org\r\n"
        << "\r\n"
        << std::flush;
    
    std::string line;
    while (std::getline(tcp_stream, line)) std::cout << line << std::endl;
    
    return 0;
}

まず、普通にビルドしてみましょう。

$ cmake .
$ time make
Scanning dependencies of target bin
[100%] Building CXX object CMakeFiles/bin.dir/main.cpp.o
Linking CXX executable bin
[100%] Built target bin
make  4.71s user 0.37s system 94% cpu 5.352 total

著者の環境(MacBook Air (11-inch, Mid 2011)、clang 3.5、最適化オプションなし)で、5.4秒かかりました。

さて、cotire を導入していきましょう。まずは、cotire をインストールします。cotire のインストールは、任意のディレクトリに設置し、include()コマンドで読み込むだけとなっています。今回はプロジェクトルートに cotire を設置してみましょう。

$ git clone https://github.com/sakra/cotire.git
project-root/
├── CMakeLists.txt
├── main.cpp
└── cotire
    ├── CMake
    │   └── cotire.cmake
    ...

そして、CMAKE_MODULE_PATHcotire/CMakeを追加し、include(cotire)とします。これで、cotire の使用する準備が整いました。cotire のもっとも基本的な使用方法は、cotire()コマンドにプリコンパイル済みヘッダーを使用したいバイナリターゲットを指定するだけです。

CMakeLists.txt
cmake_minimum_required(VERSION 3.0.2)

set(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cotire/CMake) # 追加
include(cotire)                                           # 追加

find_package(Boost REQUIRED system thread)
add_compile_options(-std=gnu++1y)
include_directories(${Boost_INCLUDE_DIRS})

include_directories(${PROJECT_SOURCE_DIR})

add_executable(bin main.cpp)
target_link_libraries(bin ${Boost_LIBRARIES})
cotire(bin) # 追加

そして、実行します。cmake には -DCOTIRE_MINIMUM_NUMBER_OF_TARGET_SOURCES=1オプションを付与します。

$ cmake -DCOTIRE_MINIMUM_NUMBER_OF_TARGET_SOURCES=1 .
$ time make
[ 20%] Generating CXX unity source cotire/bin_CXX_unity.cxx
[ 40%] Generating CXX prefix source cotire/bin_CXX_prefix.cxx
[ 60%] Generating CXX prefix header cotire/bin_CXX_prefix.hxx
[ 80%] Building CXX precompiled header cotire/bin_CXX_prefix.hxx.pch
Scanning dependencies of target bin
[100%] Building CXX object CMakeFiles/bin.dir/main.cpp.o
Linking CXX executable bin
[100%] Built target bin
make  7.50s user 0.68s system 94% cpu 8.679 total

なんと、ビルドに8.7秒かかってしまいました。これは初回はプリコンパイル済みヘッダー作成のために逆に時間がかかります。プリコンパイル済みヘッダーの威力が発揮されるのは2回目以降となります。では、ソースを変更して再度ビルドしてみましょう。

$ touch main.cpp
$ time make
Scanning dependencies of target bin
[ 20%] Building CXX object CMakeFiles/bin.dir/main.cpp.o
Linking CXX executable bin
[100%] Built target bin
make  2.47s user 0.28s system 98% cpu 2.789 total

今度は2.5秒となり、半分程度の時間でビルドすることができました。

このように、ビルド時間短縮に大きく貢献するプリコンパイル済みヘッダーを cotire を導入することによって、いとも簡単に使用することができます。なお、使用した cotire のバージョンは、1.6.7 となっています。

cotire の設定

cotire では、環境変数やプロパティによって各種設定をすることができます。cotire の設定は、cotire manual で詳しく説明されていますが、ここではよく使うと思われるものを紹介します。

プリコンパイル済みヘッダーの対象の設定

以下のディレクトリプロパティ・ターゲットプロパティは、プリコンパイル済みヘッダーの対象となるパスの追加・無視を設定するものです。

COTIRE_PREFIX_HEADER_IGNORE_PATH

このプロパティにパスを設定すると、プリコンパイル済みヘッダーの対象となるパスから除外されます。デフォルト値は、${PROJECT_SOURCE_DIR}となります。また、値はリストとして格納されるので、複数指定することができます。

COTIRE_PREFIX_HEADER_INCLUDE_PATH

このプロパティにパスを設定すると、プリコンパイル済みヘッダーの対象に追加するパスに追加されます。デフォルト値は、空文字列となります。COTIRE_PREFIX_HEADER_IGNORE_PATHと合わせて使用することが多いでしょう。なお、値はリストとして格納されるので、複数指定することができます。


set_directory_properties(PROPERTIES
  COTIRE_PREFIX_HEADER_IGNORE_PATH ${PROJECT_SOURCE_DIR}/mylib      # プロジェクトのインクルードディレクトリ
  COTIRE_PREFIX_HEADER_INCLUDE_PATH ${PROJECT_SOURCE_DIR}/externals # 外部プロジェクトのディレクトリ
)

cotire が処理を行う条件の設定

COTIRE_MINIMUM_NUMBER_OF_TARGET_SOURCES環境変数は、プリコンパイル済みヘッダーの作成対象のソース数が指定されている値より小さい場合は、なにも行いません。cotire では、プリコンパイル済みヘッダーの作成はバイナリターゲット単位となるので、依存ソース数が少ない場合には、プリコンパイル済みヘッダーのメリットを得られないばかりか、逆にコンパイル時間が増えてしまいます。この環境変数は、そんな状態を回避するために設定します。デフォルト値は 3 です。

しかし、同じソースコードを何回もビルドするときなど、有効にしたほうがよい場合もありますので、そういうときは 1 に設定します。

$ cmake -DCOTIRE_MINIMUM_NUMBER_OF_TARGET_SOURCES=1 .

cotire 使用上の注意点

ここでは、cotire を使う上での注意点を述べます。

バイナリターゲットの依存ソースにはヘッダーも指定する

cotire ではadd_executable()add_library()で生成したバイナリターゲットに対して処理をするわけですが、依存ソースには関連するヘッダーも指定しなければなりません。設定しない場合には関連するヘッダーを編集したときに依存関係が壊れてしまい、プリコンパイル済みヘッダーの生成と使用が行われなくなってしまいます。ですので以下のようにヘッダーも指定しましょう。

add_executable(bin
    main.cpp
    hoge.cpp fuga.cpp
    hoge.hpp fuga.hpp
)
cotire(bin)

COTIRE_MINIMUM_NUMBER_OF_TARGET_SOURCES の設定に気を付ける

cotire を導入したのに、プリコンパイル済みヘッダーが使用されない…と思った時は COTIRE_MINIMUM_NUMBER_OF_TARGET_SOURCES の設定を見なおしてみてください。初期値が 3 ですので、簡単なサンプルで cotire を試したい場合は、こういった罠にはまりやすいです。

cotire で生成したプリコンパイル済みヘッダーを複数のバイナリターゲットで使いまわす

cotire はその設計上、複数のバイナリターゲットでプリコンパイル済みヘッダーを使いまわすことができません。しかし、Boost.Test などで作成したテストコードのビルド、大量のサンプルコードのビルドでひとつのプリコンパイル済みヘッダーを使いまわしたいことがあります。cotire が提供している機能では、こういった要件を満たすことができません。

しかし、cotire が内部で使用しているコマンドを使うことでこれが可能となります。例えば、最初の例で示したプロジェクトのmain.cppから11個の実行ファイルを生成するとします。

まず、cotire を使わずにビルドします。

CMakeLists.txt
cmake_minimum_required(VERSION 3.0.2)

find_package(Boost REQUIRED system thread)
add_compile_options(-std=gnu++1y)
include_directories(${Boost_INCLUDE_DIRS})

include_directories(${PROJECT_SOURCE_DIR})

add_executable(bin main.cpp)
target_link_libraries(bin ${Boost_LIBRARIES})

foreach(i RANGE 9)
  add_executable(bin_alias${i} main.cpp)
  target_link_libraries(bin_alias${i} ${Boost_LIBRARIES})
endforeach()
$ cmake .
$ time make
Scanning dependencies of target bin
[  9%] Building CXX object CMakeFiles/bin.dir/main.cpp.o
Linking CXX executable bin
[  9%] Built target bin
Scanning dependencies of target bin_alias0
[ 18%] Building CXX object CMakeFiles/bin_alias0.dir/main.cpp.o
Linking CXX executable bin_alias0
[ 18%] Built target bin_alias0
...
Scanning dependencies of target bin_alias9
[100%] Building CXX object CMakeFiles/bin_alias9.dir/main.cpp.o
Linking CXX executable bin_alias9
[100%] Built target bin_alias9
make  54.48s user 4.71s system 94% cpu 1:02.80 total

次に cotire を使います。この場合、下記のようにそれぞれ10個のプリコンパイル済みヘッダーを作成してしまうので、ビルド時間が非常に長くなってしまいます。

CMakeLists.txt
cmake_minimum_required(VERSION 3.0.2)

set(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cotire/CMake)
include(cotire)

find_package(Boost REQUIRED system thread)
add_compile_options(-std=gnu++1y)
include_directories(${Boost_INCLUDE_DIRS})

include_directories(${PROJECT_SOURCE_DIR})

add_executable(bin main.cpp)
target_link_libraries(bin ${Boost_LIBRARIES})
cotire(bin)

foreach(i RANGE 9)
  add_executable(bin_alias${i} main.cpp)
  target_link_libraries(bin_alias${i} ${Boost_LIBRARIES})
  cotire(bin_alias${i})
endforeach()
$ cmake -DCOTIRE_MINIMUM_NUMBER_OF_TARGET_SOURCES=1 .
$ time make
[  0%] Generating CXX unity source cotire/bin_alias9_CXX_unity.cxx
[  1%] Generating CXX unity source cotire/bin_CXX_unity.cxx
[  2%] Generating CXX prefix source cotire/bin_CXX_prefix.cxx
[  3%] Generating CXX prefix header cotire/bin_CXX_prefix.hxx
[  3%] Building CXX precompiled header cotire/bin_CXX_prefix.hxx.pch
[  3%] Generating CXX unity source cotire/bin_alias0_CXX_unity.cxx
[  3%] Generating CXX prefix source cotire/bin_alias0_CXX_prefix.cxx
[  3%] Generating CXX prefix header cotire/bin_alias0_CXX_prefix.hxx
[  3%] Building CXX precompiled header cotire/bin_alias0_CXX_prefix.hxx.pch
...
[  7%] Generating CXX prefix source cotire/bin_alias9_CXX_prefix.cxx
[  7%] Generating CXX prefix header cotire/bin_alias9_CXX_prefix.hxx
[  7%] Building CXX precompiled header cotire/bin_alias9_CXX_prefix.hxx.pch
Scanning dependencies of target bin
[  9%] Building CXX object CMakeFiles/bin.dir/main.cpp.o
Linking CXX executable bin
[  9%] Built target bin
Scanning dependencies of target bin_alias0
[ 10%] Building CXX object CMakeFiles/bin_alias0.dir/main.cpp.o
Linking CXX executable bin_alias0
[ 18%] Built target bin_alias0
Scanning dependencies of target bin_alias1
[ 19%] Building CXX object CMakeFiles/bin_alias1.dir/main.cpp.o
Linking CXX executable bin_alias1
[ 27%] Built target bin_alias1
....
Scanning dependencies of target bin_alias9
[ 90%] Building CXX object CMakeFiles/bin_alias9.dir/main.cpp.o
Linking CXX executable bin_alias9
[100%] Built target bin_alias9
make  88.66s user 8.55s system 93% cpu 1:43.97 total

そこで、cotire 内部のコマンドを使用した、プリコンパイル済みヘッダーを複数のターゲットで使いまわすuse_shared_pchというコマンドを作成し、それぞれの実行ファイルに適用します。

CMakeLists.txt
cmake_minimum_required(VERSION 3.0.2)

set(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cotire/CMake)
include(cotire)

find_package(Boost REQUIRED system thread)
add_compile_options(-std=gnu++1y)
include_directories(${Boost_INCLUDE_DIRS})

include_directories(${PROJECT_SOURCE_DIR})

add_executable(bin main.cpp)
target_link_libraries(bin ${Boost_LIBRARIES})
cotire(bin)

# HACK: shared precompiled header
# use_shared_pch(target origin [origin_source_dir])
function(use_shared_pch target origin)
  # origin source directory
  set(origin_source_dir ${ARGV2})
  if(NOT DEFINED origin_source_dir)
    set(origin_source_dir ${CMAKE_CURRENT_SOURCE_DIR})
  endif()
  
  # origin prefix header
  get_target_property(origin_prefix_header ${origin} COTIRE_CXX_PREFIX_HEADER)
  if(NOT origin_prefix_header)
    return()
  endif()
  
  # make target pch flags
  cotire_make_pch_file_path(CXX ${origin_source_dir} ${origin} origin_pch_file)
  cotire_add_prefix_pch_inclusion_flags(
    CXX ${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION}
    ${origin_prefix_header} ${origin_pch_file}
    target_pch_flags
  )
  set_property(TARGET ${target} APPEND_STRING PROPERTY COMPILE_FLAGS ${target_pch_flags})
endfunction()

foreach(i RANGE 9)
  add_executable(bin_alias${i} main.cpp)
  target_link_libraries(bin_alias${i} ${Boost_LIBRARIES})
  use_shared_pch(bin_alias${i})
endforeach()
$ cmake -DCOTIRE_MINIMUM_NUMBER_OF_TARGET_SOURCES=1 .
$ time make
[  1%] Generating CXX unity source cotire/bin_CXX_unity.cxx
[  3%] Generating CXX prefix source cotire/bin_CXX_prefix.cxx
[  5%] Generating CXX prefix header cotire/bin_CXX_prefix.hxx
[  7%] Building CXX precompiled header cotire/bin_CXX_prefix.hxx.pch
Scanning dependencies of target bin
[  9%] Building CXX object CMakeFiles/bin.dir/main.cpp.o
Linking CXX executable bin
[  9%] Built target bin
Scanning dependencies of target bin_alias0
[ 10%] Building CXX object CMakeFiles/bin_alias0.dir/main.cpp.o
Linking CXX executable bin_alias0
[ 18%] Built target bin_alias0
Scanning dependencies of target bin_alias1
[ 20%] Building CXX object CMakeFiles/bin_alias1.dir/main.cpp.o
Linking CXX executable bin_alias1
[ 27%] Built target bin_alias1
...
Scanning dependencies of target bin_alias9
[ 92%] Building CXX object CMakeFiles/bin_alias9.dir/main.cpp.o
Linking CXX executable bin_alias9
[100%] Built target bin_alias9
make  34.25s user 3.50s system 95% cpu 39.466 total

最初に作成したプリコンパイル済みヘッダーをそれぞれのターゲットで使いまわした結果、ビルド時間の高速化に成功することができました。

しかし、これはあくまでも**"HACK"**ですので、バージョンアップにより動作しなくなる可能性があります。ですので、使用には十分注意しましょう。

おわりに

以上、CMake でのプリコンパイル済みヘッダーの作成と利用方法についてでした。cotire の導入により、ソースコードを一切変更せずにビルドの高速化を行うことが可能になるので、ぜひ導入しましょう。

明日は、mrk_21 さんの『CMake: 正規表現』です。

41
36
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
41
36

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?