exeClang 付属ライブラリの LTO サポートについて
exeGCC ではサポート範囲がユーザーコードに限定されていた LTO ですが、exeClang からはツールチェーンに付属のライブラリを含めた(一部の例外を除く)全てのコードを LTO の対象にすることができるようになりました。
本稿では、なぜ exeGCC ではそれが難しかったのか、なぜ exeClang からは可能になったのかを解説します。
注釈
LTO を使用するだけならば本稿を読む必要はありません。
exeGCC の LTO サポートについて
exeGCC の付属ライブラリは ELF ファイル( .o
拡張子)を Binutils の GNU ar でアーカイブ( .a
拡張子)したものです。これらのファイルには元のソースコードの情報が一切含まれていないため LTO はできません。そのため付属ライブラリは LTO の対象に含まれません。
GCC でも LTO は可能ですが、採用されませんでした。これは s006 まで exeGCC は 32-bit バイナリのツールチェーンで、メモリが 2GB 程度しか使用できなかったためです。LTO は小規模なプロジェクトではあまり効果が実感できず、大規模プロジェクトでは既に通常のリンクですらもメモリが足りないケースがたびたび出てきていたため、32-bit ツールチェーンでの LTO は実用的ではないとの判断でした。
その後 64-bit Widnows が普及し、弊社のデバッガも 32-bit サポートを打ち切ったこともあってツールチェーンも s007 から 64-bit 化に踏み切りましたが、以下のような様々な点を考慮し、s007 では 64-bit 化とバグフィックス以外の大きな変更は入りませんでした。
注釈
s007 に LLVM 17 の lld(Clang とバージョンが異なる理由は後述)のみを追加し、SOLID 3.3.0 から Clang の ThinLTO はサポートしました。これは 64-bit 化の恩恵に加えて、ThinLTO は省メモリでリンク時間も短い(少なくとも、1 つファイルを変更しただけで LTO が最初からやり直しにはならない)ため、十分実用的になったと判断したためです。ただし GCC の付属ライブラリに LLD を使用した場合、ARM で C++ 例外が正常動作しないなど一部制限があります。(exeClang は GCC 由来のソフトウェアを一切使用していないためこのような問題はありません。)
exeGCC のユーザーの多くが GCC よりも(サニタイザ、コードカバレッジ、XRay など SOLID がサポートする)機能が豊富な Clang を選択するようになってきた。LTO は片方しか実現できない(詳細は後述)ので GCC か Clang、どちらかを選ぶとなると Clang となる。
Clang で LTO をサポートする場合、LLVM 17 以降が必要(詳細は後述)だが、それには SOLID-OS などの対応と確認が必要なのでスケジュール的に無理であった。
そもそも exeGCC なのにライブラリまで Clang でコンパイルするとなると、バランス的におかしくなる。libgcc など GCC の付属ライブラリを Clang でコンパイルできるのかどうかが未知数な上に、前述した ARM の C++ 例外など既知の不具合もあるので、Clang の LTO をメインにするならば、LLVM と Clang で統一するべきではないか?
これらの理由により、exeGCC は現在の exeClang の形へと変化しました。
exeClang の LTO サポートについて
exeClang s008 は LLVM 18 を採用し、GCC/Binutils を使用しない、全て LLVM で完結した構成であるため、前述した exeGCC の問題は最初から存在しません。Clang の LTO も全く問題なく使用できます。
そしてツールチェーンの付属ライブラリを -flto=thin
オプションを指定してビルドしています。これによりユーザーコードだけではなく、ライブラリも LTO の対象となり、より効果的な最適化が期待できます。これが exeClang の LTO サポートの内容となります。
ライブラリビルド時には -ffat-lto-objects
オプションも併用しているので、 .a
アーカイブ内の .o
ファイルは fat LTO オブジェクトとなります。
fat LTOオブジェクトは、純粋な ELF ファイルです。ELF ファイルの特別なセクション( .lto.*
)に Clang コンパイラの中間表現を埋め込むことにより、従来の ELF 用ツールとの互換性と LTO を両立しています。
exeClang のライブラリは基本的に全て fat LTO オブジェクトなので、LTOを有効化したい場合、コンパイルオプションに -ffat-lto-objects
オプション、リンカーオプションに --ffat-lto-objects
オプションが必要です。指定しない場合 LTO は行われず、fat LTO オブジェクト内の機械語が(最適化されずに)そのままリンクされます。
注釈
fat LTO オブジェクト内の機械語は、ELFの .lto.*
セクションに格納されている中間表現を LTO 無しで従来通りにコンパイルした時のものです。LTO を行った場合、最終的に生成される機械語はソースコードと大きく異なるものとなる場合があります。そのため逆アセンブラ出力などを参照する際には、そのことを念頭に置いてください。
exeGCC では困難だった理由の詳細
付属ライブラリの LTO サポートは様々な条件を満たす必要があったため、exeClang s008 まで実現できませんでした。ここでは s007 以前では困難であった理由を解説します。(一部、既に解説した内容と重複します。)
exeGCC は GCC と Clang の 2 つのコンパイラをサポートしていた
s007 で Clang と LLVM lld による Thin LTO がサポートされましたが、LLVM 17 以前の LTO は通常のELFファイルとは異なる LTO 専用のオブジェクトファイル(LLVM IRと呼ばれる中間表現をバイトコード形式で格納したバイナリファイル。扱いには専用のツールが必要。fat との対比で slim LTO オブジェクトとも呼ばれます)を生成・使用します。これはGCC + GNU ldと互換性が無い(GCC + GNU ldの LTO は GIMPLE と呼ばれる中間表現を使用)ため、2 つのコンパイラをサポートする exeGCC ではライブラリを LTO 対象にすることはできませんでした。また、slim LTO オブジェクトは拡張子が
.o
にも関わらず ELF 形式ではなく、Binutils のツール(objdump など)が使えないため、混乱を招く恐れもありました。exeClang は Clang のみのサポートなのでこの問題はありませんでした。また、LLVM 18 からサポートされた fat LTO オブジェクトには、exeGCC と同じ Binutils がそのまま使用できるので互換性の問題もありませんでした。
注釈
exeClang の開発当初は Binutils を使用していましたが、LLVM 17 から GNU ld を lld で置き換えられるようになったので、現在は Binutils を使用していません。代わりに LLVM Tools(Binutils とほぼ同じ機能を、同じように使用できる)を Binutils の時と同じ名前にリネームしたものを同梱しています。
Clang の LTO が(ベアメタルプログラムで)実用的になったのは LLVM17(2023)から
そもそも AArch64 と ARM のベアメタルプログラムの lld によるリンクが実用レベルになったのが LLVM 17 からなので、それまでは Clang は LTO をサポートできませんでした。(GNU ld は LLVM IR をサポートしないため。)そのため s007 は、s006 から引き続き Clang は LLVM12、lld は LLVM17 という変則的な形でのサポートとなりました。(この理由は exeGCC の LTO サポートについて で解説しました。)
RISC-V(SOLID-4.0)は LLVM17 で開発を開始し、最初から lld も LTO も問題なく動作しました。
Clangの ThinLTO が実用的になるまで、LTO 自体に懐疑的であった
実は GCC が LTO 時に生成するオブジェクトファイルはもともと fat LTO だったので、s006 以前であっても、ライブラリを GCC の fat LTO オブジェクトにして、GCC のみライブラリまで LTO をサポート、Clang は LTO 未サポート(lld の成熟待ち)のような形で2つのコンパイラをサポートすることは、技術的には可能でした。
しかし当時は前述したように 32-bit のツールでの(full)LTO の実用性と得られるメリットに懐疑的であり、優先度が高くありませんでした。そして 64-bit の時代になり LTO を検討する余裕が出てきた頃には、Clang の方がメインコンパイラになりつつありました。特に RISC-V の場合、最新仕様への追従速度は Clang の方が常に優れていたので、SOLID で RISCV-V をサポートする際には自然と Clang が第一候補となり、exeGCC から今の exeClang の形へと変化して行きました。
Clangは LLVM 18(2024)まで fat LTO 未サポート(exeGCC s007 がリリースされたのは 2023 年末)
GCC は LTO をサポートした最初期 4.7(2012)から fat LTO をサポートしていましたが、なぜか LLVM は 18 までサポートしませんでした。また、GCC にも WHOle Program optimizeR(WHOPR)という Thin LTO のような(省リソースな)LTO もあったようですが、やはり 32-bit Windows 環境やベアメタルプログラムでの実績に不安があったため、exeGCC ではサポートしませんでした。
RISC-V(SOLID-4.0)は LLVM 17 で SOLID の基本部分の開発を開始し、LLVM のバージョン依存部分の開発を開始する際に LLVM 18 をサポート対象に決めたため、ツールチェーン付属ライブラリの LTO サポートを実現できました。ライブラリを含める LTO はそれなりの規模になりますが、最初から 64-bit ツールチェーンとして開発していたのでメモリ不足の不安もありませんでした。
ライブラリ自体の LTO 対応
LTO の対象をライブラリまで広げると、従来は考えられなかったような様々なバグが顕在化してしまうのではないか、そもそもコンパイラの付属ライブラリは LTO に対応しているのだろうか、なども不安要素でした。この不安は商用コンパイラテストツールを導入して網羅的にバグを検出・修正することにより解消し、現在は ThinLTO とリンク時 GC(
-Wl,--gc-sections
)は常に有効にして問題ない品質であることを確認しています。