はじめに
C++20 で、仕様上「Modules」が追加されました。
#include の代わりに export / import を使います。コンパイル時にプリプロセスで #include が展開されなくなるため、特に大きなヘッダーを #include する場合、コンパイル時間の節約が期待できます。
gcc では、最新の 16 でもオプション指定が必要ですが、実際にどの程度時間が変わるのか試してみました。
環境
- Ubuntu 24.04.3 (WSL2)
- gcc 16 (記事中で入れます)
gcc の準備
自前ビルドしてもいいのですが、楽して PPA 経由で入れます。
$ sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y
PPA publishes dbgsym, you may need to include 'main/debug' component
Repository: 'Types: deb
URIs: https://ppa.launchpadcontent.net/ubuntu-toolchain-r/test/ubuntu/
Suites: noble
Components: main
'
...
Fetched 9704 kB in 8s (1245 kB/s)
Reading package lists... Done
$ sudo apt update
Hit:1 http://archive.ubuntu.com/ubuntu noble InRelease
...
Reading state information... Done
168 packages can be upgraded. Run 'apt list --upgradable' to see them.
$ sudo apt install gcc-16 g++-16 -y
Reading package lists... Done
...
Setting up g++-16 (16-20260315-1ubuntu1~24~ppa1) ...
Processing triggers for man-db (2.12.0-4build2) ...
Processing triggers for libc-bin (2.39-0ubuntu8.7) ...
# g++ は元のまま
$ g++ --version
g++ (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0
Copyright (C) 2023 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# g++-16 で扱える
$ g++-16 --version
g++-16 (Ubuntu 16-20260315-1ubuntu1~24~ppa1) 16.0.1 20260315 (experimental) [trunk r16-8100-g3aca3bae8ee]
Copyright (C) 2026 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
自作モジュール
まず、軽くテストしてみます。
小さなモジュールとメインのファイルを用意して動かします。
この範囲は、C++20(gcc16のデフォルト)でも動くと思います。
// モジュール名: test
export module test;
// モジュール test でエクスポートする関数
export int add(int a, int b) {
return a + b;
}
import test;
#include <iostream>
int main() {
std::cout << add(1, 2) << std::endl;
return 0;
}
# モジュールをビルド
# `-fmodules-ts` オプションが必要
$ g++-16 -std=c++23 -fmodules-ts -c testmod.cppm
# いつも通りの `.o` と、`gcm.cache`(ディレクトリ)ができる
$ ls
gcm.cache testmod.cppm testmod.o testmain.cpp
# ディレクトリ内にモジュールの情報ができる
$ ls gcm.cache
test.gcm
# 実行モジュールをビルド
$ g++-16 -std=c++23 -fmodules-ts testmain.cpp testmod.o -o testmain
# 実行
$ ./testmain
3
std 系のモジュールとしての利用
C++23 では、標準ライブラリをモジュール化して扱えるようになります。
(自分でビルドは必要)
// std 系がすべて使えるようになる
import std;
int main () {
std::cout << "Hello, world!" << std::endl;
return 0;
}
# ./gcm.cache/std.gcm ができる
$ g++-16 -std=c++23 -fmodules-ts -c /usr/include/c++/16/bits/std.cc
$ g++-16 -std=c++23 -fmodules-ts teststd.cpp -o stdmain
$ ./stdmain
Hello, world!
ちなみに、import stdではなく#include <iostream>したバージョンよりすこしだけ小さくなりました。
$ ls -l
-rwxr-xr-x 1 dev dev 16264 Jun 21 22:37 stdmain2 # import std の方
-rwxr-xr-x 1 dev dev 16336 Jun 21 22:40 stdmain3 # #include <iostream> の方
また、std::printlnを使ったものはimport stdと#include <print>で同じバイト数でした。
利用する関数でちょっと差がありますがほぼほぼ同じようです。
ちなみにlddでリンクされる共有ライブラリを見たところ差がなかったので、モジュールを使ったかどうかはバイナリを見ても分からない気がします。
大きなヘッダーのライブラリ
json.hpp をお借りします。#include するだけで使えるJSONのヘッダーオンリーのライブラリです。
931kBあります。
モジュールなし
ざっと簡単なソースを用意します。
#include <iostream>
#include "json.hpp"
int main() {
nlohmann::json j;
j["key"] = "value";
std::cout << j.dump() << std::endl;
return 0;
}
何回かビルドしてみましたが、2秒ほどかかりました。
$ time g++-16 -std=c++23 testjson.cpp
g++-16 -std=c++23 testjson.cpp 1.87s user 0.34s system 96% cpu 2.282 total
$ ./a.out
{"key":"value"}
モジュールあり
module;
#include "json.hpp"
export module json;
export namespace json {
using json = nlohmann::json;
}
import std;
import json;
int main() {
json::json j;
j["key"] = "value";
std::cout << j.dump() << std::endl;
return 0;
}
# 警告が出るし3-4秒かかる
$ time g++-16 -std=c++23 -fmodules-ts -c jsonmod.cppm
In file included from jsonmod.cppm:3:
json.hpp:15170:34: warning: ‘static std::vector<typename nlohmann::json_abi_v3_12_0::json_pointer<RefStringType>::string_t_helper<RefStringType>::type> nlohmann::json_abi_v3_12_0::json_pointer<RefStringType>::split(const string_t&) [with RefStringType = std::__cxx11::basic_string<char>; typename string_t_helper<RefStringType>::type = std::__cxx11::basic_string<char>; string_t = std::__cxx11::basic_string<char>]’ exposes TU-local entity ‘void nlohmann::json_abi_v3_12_0::detail::unescape(StringType&) [with StringType = std::__cxx11::basic_string<char>]’ [-Wexpose-global-module-tu-local]
15170 | static std::vector<string_t> split(const string_t& reference_string)
| ^~~~~
json.hpp:3132:13: note: ‘void nlohmann::json_abi_v3_12_0::detail::unescape(StringType&) [with StringType = std::__cxx11::basic_string<char>]’ is a specialization of TU-local template ‘template<class StringType> void nlohmann::json_abi_v3_12_0::detail::unescape(StringType&)’
3132 | static void unescape(StringType& s)
| ^~~~~~~~
json.hpp:3132:13: note: ‘template<class StringType> void nlohmann::json_abi_v3_12_0::detail::unescape(StringType&)’ declared with internal linkage
g++-16 -std=c++23 -fmodules-ts -c jsonmod.cppm 2.82s user 0.45s system 99% cpu 3.279 total
# モジュールのリンクで1.5秒くらいかかる
$ time g++-16 -std=c++23 -fmodules-ts testjson.cpp jsonmod.o
g++-16 -std=c++23 -fmodules-ts testjson.cpp jsonmod.o 1.16s user 0.23s system 95% cpu 1.455 total
若干早くなりましたがそんなに劇的じゃないですね。
メインのファイルからimport std;と出力の行を消すと半分くらいのビルド時間になるので、標準ライブラリもぼちぼち重いようです。
メインのファイルのコンパイルとリンクを分けてやってみると、コンパイルに時間かかってました。
結論
テンプレート系巨大ヘッダーは、結局使う側で実体化するときのコンパイルコストがあるせいであんま早くならないっぽいです。
ちゃっとじーぴーてぃーさんによると、中くらいのヘッダー(どのくらいだ?)がたくさんあるときにモジュール化が効きやすいらしいです。