5月13日、Vittorio Romeoが「cost of enum-to-string: C++26 reflection vs the old ways」と題した記事を公開した。この記事では、C++26で導入予定のリフレクション機能を使ったenum-to-string変換のコンパイル時間コストを従来手法と比較検証したベンチマーク結果について詳しく紹介されている。以下に、その内容を紹介する。
C++26リフレクション:革新的だが現実的なコストは?
C++26で導入されるリフレクション機能は、メタプログラミングの世界を大きく変える可能性がある。テンプレートメタプログラミングの複雑さを解消し、直感的なコードでコンパイル時プログラミングが可能になると期待されているが、実際のプロジェクトでの採用を考える際に重要なのはコンパイル時間への影響である。
著者は2か月前にC++26リフレクションのコンパイル時コストについて記事を公開したが、今回はより実践的な例であるenum-to-string変換でベンチマークを実施した。enum-to-string変換はリフレクションの「Hello World」的存在だが、ログ出力、シリアライゼーション、デバッグなど実際のプロジェクトで有用な機能でもある。リフレクションを実際のコードベースで採用する際、最初に実装する機能の一つになるだろう。
3つのアプローチを比較
著者は同じ機能(enum値からstd::string_viewで列挙子名を返す)を3つの手法で実装し、コンパイル時間を測定した。
1. リフレクション(C++26)
マクロ不要、定型コード不要で任意のenumに対応:
#include <meta>
#include <string_view>
#include <type_traits>
template <typename T>
requires std::is_enum_v<T>
constexpr std::string_view to_enum_string(T val)
{
template for (constexpr auto e :
std::define_static_array(std::meta::enumerators_of(^^T)))
{
if (val == [:e:])
return std::meta::identifier_of(e);
}
return "<unknown>";
}
2. enchantum(ヘッダーオンリライブラリ)
__PRETTY_FUNCTION__パーシング技法でenumリフレクションを実現するC++17ライブラリ:
#include <enchantum/enchantum.hpp>
enum class E { V0, V1, V2, V3 };
std::string_view s = enchantum::to_string(E::V0);
3. X-macro(プリプロセッサ)
C言語スタイルの解決法。列挙子を一度リストアップすると、単一のマクロがenum class定義とswitch文を使うto_string関数の両方に展開される:
#define E_LIST(X) \
X(V0) X(V1) X(V2) X(V3)
DEFINE_ENUM(E, E_LIST)
ベンチマーク結果の衝撃的な違い
各手法について、4、16、64、256、1024個の列挙子を持つenumでテストした結果は以下の通りだ:
| N列挙子数 | X-macro (const char*) | X-macro (string_view) | enchantum | リフレクション |
|---|---|---|---|---|
| ヘッダーインクルードのみ | 25.7 ms | 136.0 ms | 147.1 ms | 180.8 ms |
| 4 | 26.6 ms | 137.6 ms | 170.6 ms | 186.7 ms |
| 16 | 26.9 ms | 138.1 ms | 170.9 ms | 187.7 ms |
| 64 | 28.0 ms | 141.2 ms | 172.8 ms | 191.1 ms |
| 256 | 32.5 ms | 153.0 ms | 184.1 ms | 215.0 ms |
| 1024 | 54.7 ms | 204.5 ms | 272.0 ms | 255.0 ms |
プリコンパイル済みヘッダーが状況を一変
リフレクション版は<meta>ヘッダーで約155msのコストを支払うため、プリコンパイル済みヘッダー(PCH)とC++20モジュールの効果も測定した:
| N列挙子数 | リフレクション(通常) | リフレクション + PCH | リフレクション + モジュール |
|---|---|---|---|
| 4 | 186.7 ms | 80.6 ms | 403.4 ms |
| 16 | 187.7 ms | 81.0 ms | 403.1 ms |
| 64 | 191.1 ms | 84.4 ms | 409.4 ms |
PCHは約2.3倍の高速化を実現し、リフレクション版を他の手法より高速にした。一方、モジュールは約2.2倍の低速化という意外な結果となった。
実際のコードベースへの影響
単一の翻訳単位では小さく見える数値も、スケールすると大きな差になる。500個の翻訳単位を持つ大規模C++コードベースで、N=16のenumを想定すると:
| アプローチ | 翻訳単位あたり | プロジェクト全体(500TU) |
|---|---|---|
| X-macro (const char*) | 26.9 ms | 約13秒 |
| リフレクション | 187.7 ms | 約94秒 |
翻訳単位あたり数百ミリ秒の差が、プロジェクト全体では1分以上の差となる。これが15秒以下のクリーンビルドと1分半のビルドの違いを生む。
重要な洞察
コストはリフレクション処理ではなくヘッダーにある:リフレクションアルゴリズム自体は高速で、列挙子あたり約0.07msと手書きswitch文とほぼ同等である
const char*を返すX-macroが最速:標準ライブラリヘッダーなしでN=4のenumが26.6msでコンパイルできるPCHが状況を改善、モジュールは悪化:プリコンパイル済みヘッダーでリフレクションが最速になるが、C++20モジュールは逆効果
C++26リフレクションの現実的な採用戦略
この結果から、大規模コードベースでリフレクション版enum-to-stringを採用する場合の推奨事項が見えてくる:
<meta>にはPCHを使用し、モジュールは避ける- enum-to-stringヘッダーを他のヘッダーからインクルードしない
- コンパイル時間重視なら、X-macroが依然として正解
- ライブラリ作者は、パブリックヘッダーでのリフレクション公開を慎重に検討する
C++26リフレクションは確かに強力だが、実際のプロジェクトでの採用には慎重な設計が必要になりそうだ。特に大規模なコードベースでは、ビルド時間への影響を十分に考慮した導入戦略が求められる。
詳細はcost of enum-to-string: C++26 reflection vs the old waysを参照していただきたい。