パソコンでプログラミングしよう ウィンドウズC++プログラミング環境の構築
1.7.0.5(5)
mingw-w64のプリコンパイル済みヘッダ

mingw-w64のプリコンパイル済みヘッダをCode::Blocks設定と共に説明し、コンパイル時間の短縮効果を測定する。

プリコンパイル済みヘッダとはコンパイル時間短縮を目的にインクルードファイルを前処理(プリコンパイル)してできる中間ファイルの事で、ソースコードファイルは元々のインクルードファイルの代わりにプリコンパイル済みヘッダを使う。mingw-w64を含め現代的なC++コンパイラのほとんどがこの機能を用意し、それぞれが規格の拡張として独自用法で実装する。mingw-w64などは翻訳単位毎にプリコンパイル済みヘッダを変える事ができる一方、プロジェクトでただ一つのプリコンパイル済みヘッダを持つ実装も存在する。本サイトはmingw-w64を使うもののプリコンパイル済みヘッダはプロジェクトで一つを原則として全ての翻訳単位がこれを共用するが、これはサイト作成者が以前に使用していたコンパイラの制限を無意識に受け継いでしまった結果である。

mingw-w64

mingw-w64のプリコンパイル済みヘッダを簡単にまとめる。mingw-w64はプリコンパイル済みヘッダを以下に生成する(GCC 14.1.0 Manual 3.2 Options Controlling the Kind of Output)。

  • コンパイラに拡張子.hを持つファイル(例えばpch.h)を入力に与えるとプリコンパイル済みヘッダを出力する。
  • 出力ファイル名を-oオプションで指定しない場合、拡張子含む入力ファイル名にさらに拡張子.gchを加えたファイル名(pch.h.gch)をデフォルトとする。

翻訳単位はコンパイル時に生成済みのプリコンパイル済みヘッダを利用できる(GCC 14.1.0 Manual 3.22 Using Precompiled Headers)。

  • 翻訳単位は最大で一つのプリコンパイル済みヘッダを利用できる。
  • Cトークン(C言語/C++の最小構文要素)より後の#includeはプリコンパイル済みヘッダを利用しない。
  • インクルードファイル内の#includeはプリコンパイル済みヘッダを利用しない。
  • 前三条件を合わせれば、プリコンパイル済みヘッダ利用が可能なのはソースコードファイル先頭の#includeにほぼ限定される。
  • #includeがプリコンパイル済みヘッダ利用が可能なpch.hを探索する場合、インクルードパスの各ディレクトリ毎にpch.h.gchとpch.hの順番で探索する。
  • pch.h.gchが見つかり、それがファイルであればプリコンパイル済みヘッダと見なしてインクルードを試みる。
  • pch.h.gchが見つかり、それがサブディレクトリであれば含まれる全ファイルを名前に関係なくプリコンパイル済みヘッダファイルと見なして順次インクルードを試みる。
  • プリコンパイル済みヘッダのインクルードは以下の条件で成功する。
    • プリコンパイル済みヘッダと翻訳単位は同一コンパイラ、同一コンパイルオプションでプリコンパイル/コンパイルする。
    • プリコンパイル済みヘッダのマクロ定義は翻訳単位のそれと同一である。
  • プリコンパイル済みヘッダのインクルードに失敗したら次候補を試みる。次候補がpch.hなら普通にインクルードしてプリコンパイル済みヘッダを利用しない。

Code::Blocks

Code::Blocksでmingw-w64のプリコンパイル済みヘッダを利用する方法をまとめる。あるプロジェクトのpch.hインクルードファイルをプリコンパイル対象とする。pch.hのプリコンパイルを設定し、プロジェクト設定でプリコンパイル済みヘッダ利用方法を選択する。

  • [Management]ウィンドウ[Projects]ページでpch.hノードを右クリックして[Properties]コンテキストメニューから[Properties of "pch.h"]ダイアログを開く。[Build]ページで[Compile file]をチェックし[Belongs in targets]の利用ターゲット全てにチェックされている事を確認して、[Priority weight]を0にセットする。
  • [Project|Properties]で開く[Project/target options]ダイアログ[Project settings]ページの[Precompiled headers|Strategy]ラジオボタンでコンパイル済みヘッダ利用方法を選択する。
覚え書き
pch.hはソースコードファイル(翻訳単位)コンパイルより前にプリコンパイルする必要があるため、[Priority weight]で優先順位を上げる。ソースコードファイルの[Priority weight]は恐らくデフォルト50のままなので、pch.hは念のため最高優先順位として0を設定する。MSYS2のmakeコマンドラインツールならこういった依存関係を明確に定義できるが、Code::Blocksプロジェクトは曖昧な方法を採らざるを得ない。

[Precompiled headers|Strategy]選択はプリコンパイル済みヘッダ出力を定義し、プリコンパイル/翻訳単位コンパイルに共通でインクルードパスオプションを追加する。一例としてDebug32ターゲットの場合をまとめる。

Generate PCH... プリコンパイル済みヘッダ インクルードパスオプションの追加
in a directory alongside original header pch.h.gch\Debug32_pch_h_gch (無し)
in the object output dir obj\Debug32\pch.h.gch -iquoteobj\Debug32 -Iobj\Debug32 -I.
alongside original header (default) pch.h.gch (無し)

in the object output dirのインクルードパスオプション追加は不具合を抱える。これはプリコンパイル済みヘッダをオブジェクトファイルディレクトリ(obj\Debug32)に出力して、翻訳単位コンパイルのインクルードパスでobj\Debug32をプロジェクトディレクトリ(.)に優先させる事を意図する。これで#include <pch.h>はpch.h.gchをインクルードする一方、#include "pch.h"はpch.hをインクルードする。なぜなら#include "pch.h"はソースコードファイルディレクトリが常にインクルードパス先頭にあり、そこでpch.h.gchを発見できない代わりにpch.hを発見する。コンパイラは正常にインクルードファイルを発見するので警告する謂れは無く、そのためプリコンパイル済みヘッダを利用していない事に気付かない。この不具合も考慮してプリコンパイル済みヘッダの利用方法を以下にまとめる。

Generate PCH... 出力ディレクトリ インクルード指令
in a directory alongside original header pch.hと同じディレクトリのサブディレクトリ #include "pch.h"
in the object output dir オブジェクトファイル出力ディレクトリ #include <pch.h>
alongside original header (default) pch.hと同じディレクトリ #include "pch.h"

pch.hと同じディレクトリのサブディレクトリに出力する場合はターゲット別にプリコンパイル済みヘッダが作成され、翻訳単位のコンパイル時にはディレクトリ内ファイルのインクルードを順次試みる。オブジェクトファイル出力ディレクトリに出力する場合もターゲット別にプリコンパイル済みヘッダが作成され、翻訳単位コンパイル時にはディレクトリ指定されて正しいファイルをインクルードする。pch.hと同じディレクトリに出力する場合はターゲット変更の度にプリコンパイル済みヘッダを作成しなおす。

覚え書き
普通はオリジナル(pch.h)がインクルードパスに残るためプリコンパイル済みヘッダが本当に利用されたかどうか確認するのは意外に難しい。以下二つが考えられるもののビルドシステムに組み込むのは困難で、コマンドプロンプトで逐次マニュアル操作するか専用のバッチファイルを用意する必要がある。
  • プリコンパイル済みヘッダ作成後にpch.hに#error指令を追加して、プリコンパイル済みヘッダを利用できなかった場合にエラーを起こさせる。ただしCode::Blocksを含む通常のビルドシステムはプリコンパイル済みヘッダをpch.hに依存して再作成してしまう。
  • コンパイラ-Eオプションに-fpch-preprocessオプションを加えてプリプロセッサ出力を得る。-fpch-preprocessはプリコンパイル済みヘッダの使用箇所をプリプロセッサ出力内に明示する。プリプロセッサ出力作成にあたっては実際のビルド時のインクルードパスを正しく与える必要がある。

複数バージョンのライブラリが共存する場合

boostwxWidgetsで示すように同一ライブラリの複数バージョンを別ディレクトリにインストールする場合、プロジェクトターゲットでこれを切り替える事ができる。[Project|Build options]の[Project build options]ダイアログで左側ツリービューでターゲットを選択し、[Search directories]ページの[Compiler]、[Linker]、[Resource compiler]を適切に設定すれば良いが、mingw-w64に渡す-Iオプションが変更されるもののコンパイルオプションは変更されずプリコンパイル済みヘッダは同一と認識される。同一アーキテクチャの異なるターゲットでライブラリバージョンを変える場合、バージョン依存でライブラリインクルードが異なればエラーを発生する。

これを避けるにはプリコンパイル済みヘッダ出力ディレクトリをオブジェクトファイル出力ディレクトリ(in the object output dir)とすれば良いが、その場合はインクルードを#include <pch.h>とする必要がある。出力ディレクトリを他(in a directory alongside original headerあるいはalongside original header (default))として#include "pch.h"とするには、それぞれに異なるマクロ定義を加えれば良い。最も簡便には左側ツリービュールートノードのプロジェクトオプションを選択して[Compiler settings|#defines]ページにビルトイン変数$(TARGET_NAME)を追加しておけば、各ターゲットにターゲット名を持つマクロが自動的に定義される。

wxWidgets

wxWidgetsライブラリを使用するプログラムがプリコンパイル済みヘッダを利用するにはWX_PRECOMPマクロを定義して次のインクルードファイル(wx_pch.h)をプリコンパイル対象とする。Code::Blocksに添付するwxWidgets projectウィザード、本サイトが提案するwxWidgetsプロジェクトウィザード(K1)/wxWidgetsプロジェクトウィザード(K2)はwx_pch.hを含むプロジェクトを生成する。コンパイラ時間をより短縮するには、それぞれの翻訳単位が用いるライブラリインクルード全てを// put here all your realy-changing header filesに移す。

#ifndef WX_PCH_H_INCLUDED
#define WX_PCH_H_INCLUDED
// basic wxWidgets headers
#include <wx/wxprec.h>
#ifdef __BORLANDC__
#pragma hdrstop
#endif
#ifndef WX_PRECOMP
#include <wx/wx.h>
#endif
#ifdef WX_PRECOMP
// put here all your rarely-changing header files
#endif // WX_PRECOMP
#endif // WX_PCH_H_INCLUDED

wx/wxprec.hはwxWidgetsの供給するインクルードファイルで以下に示す。

// Name: wx/wxprec.h
// Purpose: Includes the appropriate files for precompiled headers
// Author: Julian Smart
// Modified by:
// Created: 01/02/97
// Copyright: (c) Julian Smart
// Licence: wxWindows licence
// compiler detection; includes setup.h
#include "wx/defs.h"
// check if to use precompiled headers: do it for most Windows compilers unless
// explicitly disabled by defining NOPCH
#if defined(__VISUALC__) || defined(__BORLANDC__)
// If user did not request NOCPH and we're not building using configure
// then assume user wants precompiled headers.
#if !defined(NOPCH) && !defined(__WX_SETUP_H__)
#define WX_PRECOMP
#endif
#endif
#ifdef WX_PRECOMP
// include "wx/chartype.h" first to ensure that UNICODE macro is correctly set
// _before_ including <windows.h>
#include "wx/chartype.h"
// include standard Windows headers
#if defined(__WINDOWS__)
#include "wx/msw/wrapwin.h"
#include "wx/msw/private.h"
#endif
#if defined(__WXMSW__)
#include "wx/msw/wrapcctl.h"
#include "wx/msw/wrapcdlg.h"
#include "wx/msw/missing.h"
#endif
// include the most common wx headers
#include "wx/wx.h"
#endif // WX_PRECOMP

WX_PRECOMPを定義するとwx_pch.hがwx/wx.hをインクルードしない代わりにwx/wxprec.hがwx/wx.hをインクルードする。この不自然な処置は複数のウィンドウズコンパイラに対応するための経験に基づくおまじないだそうだ。wx/wx.hはおよそ80個の主要ファイルをインクルードして、wx_pch.hはデフォルトで既に多くのインクルードファイルを含む。

効果の測定

プリコンパイル済みヘッダによるコンパイル時間短縮効果を測定する。測定対象はwxWidgets利用のサンプルコードで翻訳単位9、総行数およそ2500、ハードウェアはCore i5-3210M(2.50GHz)、メモリ8GiB、SSDストレージである。Code::Blocksプロジェクトは依存関係を制御できずかつ自動テスト構築が難しいため、コンパイルに必要な最小限のソースコードを新たなディレクトリ(...\KTxtEdit_pch_test)へコピーしてMSYS2のmakeとバッチファイルを活用する。ディレクトリ配置は以下とするが、丸括弧で表示したディレクトリ/ファイルはテスト水準が作成するもので次水準(objディレクトリのみ次試行)はこれらを最初に消去する。

ディレクトリ ファイル 内容
...\KTxtEdit_pch_test *.cpp ソースコード
*.h インクルード
resource.rc ウィンドウズリソース
wx_pch.h プリコンパイル対象インクルード
pch_test.bat テスト用バッチ
Makefile メイクファイル
out.dat 測定結果出力
(wx_pch.h.gch) プリコンパイル済みヘッダ
├ iconimages favicon.ico ファビコン
*.xpm メニュー項目ビットマップ
├ (obj) (*.o) オブジェクト
└ (wx_pch.h.gch) (X01_wx_pch_h_gch) プリコンパイル済みヘッダ
(X02_wx_pch_h_gch) ...
(...) ...

テスト水準

テストはプリコンパイル済みヘッダを利用する場合は事前作成しておき、サンプルコードの全翻訳単位をコンパイルする時間を計測し、すなわちリンクはテストに含まない。以下の4水準×4水準とし、5回試行して平均値を採る。プリコンパイル済みヘッダ因子のPCHサブディレクトリ水準はサブディレクトリにマクロ定義を変更する10個のプリコンパイル済みヘッダを作成し、コンパイルはその内一つのマクロを定義して利用する。

因子 水準 説明
コンパイラ mingw32(-g) 32ビット-gオプション、Debug32ターゲット相当
mingw32(-O2) 32ビット-O2オプション、Release32ターゲット相当
mingw64(-g) 64ビット-gオプション、Debug64ターゲット相当
mingw64(-O2) 64ビット-O2オプション、Release64ターゲット相当
プリコンパイル済みヘッダ PCH無し プリコンパイル済みヘッダを利用しない
PCHデフォルト wx_pch.hをライブラリインクルードを追加しないデフォルトのまま、wx_pch.h.gchをwx_pch.hと同じディレクトリに作成する
PCH 全翻訳単位のライブラリインクルードを追加して、wx_pch.h.gchをwx_pch.hと同じディレクトリに作成する
PCHサブディレクトリ 全翻訳単位のライブラリインクルードを追加して、wx_pch.h.gchサブディレクトリに10個プリコンパイル済みヘッダを作成する

プリコンパイル対象ファイル

より多くのインクルードをプリコンパイルしたほうがより高速化を期待できるため、全翻訳単位のライブラリインクルードをwx_pch.hにコピーする。これらは#ifndef NO_RARELY_CHANGING_HEADER_FILESで囲みライブラリインクルードを追加しない場合もテストする。これを理由にコピーしたライブラリインクルードは各翻訳単位に残す。このwx_pch.hはpimplイディオムが使うMY_PIMPLマクロ定義も含むが、本記事では関係ない。

wx_pch.h

#ifndef WX_PCH_H_INCLUDED
#define WX_PCH_H_INCLUDED
// basic wxWidgets headers
#include <wx/wxprec.h>
#ifdef __BORLANDC__
#pragma hdrstop
#endif
#ifndef WX_PRECOMP
#include <wx/wx.h>
#endif
#ifdef WX_PRECOMP
#ifndef NO_RARELY_CHANGING_HEADER_FILES
// put here all your rarely-changing header files
//... std
#include <memory>
#include <vector>
#include <list>
#include <set>
#include <map>
#include <algorithm>
#include <functional>
#include <fstream>
#include <locale>
//... posix
#include <iconv.h>
//... boost
#include <boost/mpl/list.hpp>
#include <boost/mpl/for_each.hpp>
//... wxWidgets
#include <wx/app.h>
#include <wx/frame.h>
#include <wx/button.h>
#include <wx/checkbox.h>
#include <wx/choice.h>
#include <wx/dialog.h>
#include <wx/panel.h>
#include <wx/sizer.h>
#include <wx/stattext.h>
#include <wx/textctrl.h>
#include <wx/notebook.h>
#include <wx/spinctrl.h>
#include <wx/msgdlg.h>
#include <wx/fontdlg.h>
#include <wx/colordlg.h>
#include <wx/stdpaths.h>
#include <wx/filename.h>
#include <wx/help.h>
#include <wx/msw/registry.h>
#endif
#endif // WX_PRECOMP
#include <memory>
#define MY_PIMPL(smart_ptr,cls,name) class cls;smart_ptr<cls> name;
#endif // WX_PCH_H_INCLUDED

メイクファイル

テストはCode::Blocksで行わずmakeを用い、Makefileファイルはそのタスクを定義する。用いるMakefileは次の環境変数定義を必要とする。

環境変数 内容
MINGWPATH MSYS2サブシステムパス C:\msys64\mingw32
OPTIMIZE 最適化オプション -g, -O2
PREDEFINE 事前マクロ定義 -DWX_PRECOMP

このMakefileは以下のフォニー(PHONY)ターゲットを持つ。テストはclean、precompiled、precompiled_2、compileを使用する。

ターゲット 機能
link オブジェクトファイルから実行形式ファイルをリンクする
compile ソースコードファイルからオブジェクトファイルをコンパイルする
precompiled wx_pch.hからプリコンパイル済みヘッダwx_pch.h.gchを作成する
windres resoruce.rcリソーススクリプトからresoruce.resリソースを作成する
clean 実行形式、オブジェクト、プリコンパイル済みヘッダなどを削除する
precompiled_2 wx_pch.hからマクロIDENTITY=X01~X10とする10個のプリコンパイル済みヘッダをwx_pch.h.gchサブディレクトリに作成する

Makefile

SOURCES=$(wildcard *.cpp)
OBJECTS=$(SOURCES:%.cpp=obj/%.o)
INCLUDEDIRS=-I$(MINGWPATH)/lib/wx/include/msw-unicode-static-3.0 -I$(MINGWPATH)/include/wx-3.0
CXX=g++
CFLAGS=-Wall -pipe -mthreads $(OPTIMIZE)
#CFLAGS=-Wall -pipe -mthreads $(OPTIMIZE) -Winvalid-pch
CPPFLAGS=$(PREDEFINE) $(INCLUDEDIRS)
WINDRESFLAGS=$(INCLUDEDIRS) -J rc -O coff
LDFLAGS=-mthreads -static -mwindows
LDLIBS=-lwx_mswu_core-3.0 -lwx_baseu-3.0 -ltiff -ljpeg -lwxpng-3.0 -lz -lzstd -llzma -liconv -lkernel32 -luser32 -lgdi32 -lwinspool -lcomdlg32 -ladvapi32 -lshell32 -lole32 -loleaut32 -luuid -loleacc -lcomctl32 -lwsock32 -lodbc32 -lshlwapi -lversion
.PHONY:link
link:bin/out.exe
bin/out.exe:$(OBJECTS) obj/resource.res
$(CXX) $(LDFLAGS) -o $@ $(OBJECTS) obj/resource.res $(LDLIBS)
.PHONY:compile
compile:$(OBJECTS)
obj/%.o:%.cpp wx_pch.h
$(CXX) -c $(CFLAGS) $(CPPFLAGS) -o $@ $<
.PHONY:precompiled
precompiled:wx_pch.h.gch
wx_pch.h.gch:wx_pch.h
$(CXX) -c $(CFLAGS) $(CPPFLAGS) -o $@ $<
.PHONY:windres
windres:obj/resource.res
obj/resource.res:resource.rc
windres.exe $(WINDRESFLAGS) -i resource.rc -o obj/resource.res
.PHONY:clean
clean:
rm -rf wx_pch.h.gch
rm -f bin/out.exe $(OBJECTS) wx_pch.h.gch obj/resource.res
IDENTITIES=$(shell seq -f X%02.0f 1 10)
PCHFILES=$(IDENTITIES:%=wx_pch.h.gch/%_wx_pch_h_gch)
.PHONY:precompiled_2
precompiled_2:$(PCHFILES)
wx_pch.h.gch/%_wx_pch_h_gch:wx_pch.h
mkdir -p wx_pch.h.gch
$(CXX) -c $(CFLAGS) $(CPPFLAGS) -DIDENTITY=$* -o $@ $<
覚え書き
これをコピペで利用する場合は行頭ホワイトスペースをMakefile書式に合致させる。具体的には字下げしない行頭にホワイトスペース文字があってはならず、字下げはタブ文字とする。

テスト実行バッチファイル

4水準×4水準×5回のテストをmakeで自動実行する。コンパイル時間の計測にMSYS2のtimeコマンドラインツール、一部出力のファイル書き出しにteeコマンドラインツールを用いる。結果はout.datファイルに追記する。

pch_test.bat

%_FILEOUT%は標準出力をout.datに追記する。forループ内の%PATH%は丸括弧を含むとエラーとなるため遅延評価の!PATH!を用いる。timeはウィンドウズシェルコマンドと名前が衝突するため"time"としてファイル実行を明示する。timeは結果をエラー出力するため標準出力に切り替える一方で、make compileの標準出力はout.datに不要なのでcon:に切り替える。

@setlocal enabledelayedexpansion
@mkdir obj 2>nul
@set _MSYSPATH=C:\msys64
@set PATH=%_MSYSPATH%\usr\bin;%PATH%
@set _FILEOUT=^|tee --append out.dat
@echo.%_FILEOUT%
@echo PRECOMPILED HEADER TEST %date% %time%%_FILEOUT%
@for %%t in (mingw32 mingw64) do @(
set MINGWPATH=%_MSYSPATH%\%%t
set PATH=!MINGWPATH!\bin;!PATH!
for %%o in (-g -O2) do @(
set OPTIMIZE=%%o
echo.%_FILEOUT%
echo %%t ^(%%o^)%_FILEOUT%
echo.%_FILEOUT%
for /l %%s in (1,1,4) do @(
make clean
if %%s==1 (
echo [Without precompiled header]%_FILEOUT%
set PREDEFINE=
) else if %%s==2 (
echo [With precompiled header no rarely-changing files]%_FILEOUT%
set PREDEFINE=-DWX_PRECOMP -DNO_RARELY_CHANGING_HEADER_FILES
make precompiled
) else if %%s==3 (
echo [With precompiled header]%_FILEOUT%
set PREDEFINE=-DWX_PRECOMP
make precompiled
) else if %%s==4 (
echo [With precompiled headers in subdirectory]%_FILEOUT%
set PREDEFINE=-DWX_PRECOMP
make precompiled_2
set PREDEFINE=-DWX_PRECOMP -DIDENTITY=X10
) else (
echo Invalid operation required.>&2
exit /b 1
)
for /l %%n in (1,1,5) do @(
del obj\*.o 2>nul
"time" -f "N=%%n: %%e sec." make compile 2>&1 1>con:%_FILEOUT%
)
)
)
)
@exit/b

テスト結果

コンパイラ PCH無し PCHデフォルト PCH PCHサブディレクトリ
mingw32(-g) 48.45秒 15.74秒 14.47秒 14.65秒
mingw32(-O2) 50.27秒 17.93秒 16.60秒 16.66秒
mingw64(-g) 58.97秒 17.71秒 16.27秒 16.55秒
mingw64(-O2) 61.07秒 18.68秒 17.18秒 17.44秒

wx_pch.hはデフォルトで多くのインクルードファイルを含むため、PCHデフォルトでもコンパイル時間は1/3程度に短縮する。短縮効果はコンパイラに依存しない。全翻訳単位のライブラリインクルードを追加したPCHはコンパイル時間を短縮するが効果は大きくない。追加したインクルードファイルが比較的少なく、追加したファイルのいくつかはデフォルトで既にインクルードされているためと考える。プリコンパイル済みヘッダをサブディレクトリ内で探索するPCHサブディレクトリはコンパイル時間を増加するが無視できるレベルにある。