GCC optimization/ja

このガイドは、Article description::コンパイル済みコードを安全で分別のある CFLAGS と CXXFLAGS を使って最適化する手法を紹介します. また、一般的な最適化の背景にある理論について述べます.

CFLAGSとCXXFLAGSとは？
CFLAGS と CXXFLAGS は、CやC++のソースコードをコンパイルするときに使われるオプションを、コンパイラに指示するために慣例的に使われる環境変数の一種です. これらの環境変数は標準化されているわけではありませんがそれに近い状況で用いられており、コンパイラを使う際に追加のオプションを指定したいなら理解しておく必要があります. GNU makeでは一般的によく使われている環境変数のリストが記されています.

Gentooシステムでは大多数のパッケージがCまたはC++で書かれているため、これらの環境変数はシステムがどのように構成されるかに多大な影響を及ぼします. そのため管理者にはこれらを正しく設定することが求められます.

これらはプログラムのデバッグメッセージの量を減らすために使われたり、エラーや警告のレベルを増加させたり、また、もちろん生成されるコードの最適化にも使われます. GCCのマニュアルに利用可能なフラグとその効果の完全なリストが記載されています.

どのように使われているのでしょうか？
普通 CFLAGS と CXXFLAGS は、configureスクリプトを走らせるか、プログラム毎ににより生成されたMakefileによってセットされます. Gentooシステムでは、 CFLAGS と CXXFLAGS はで設定します. このファイルに定義された変数はportageが呼び出したプログラムに適用され、その設定に基づいてプログラムがコンパイルされます.

見てわかるとおり、 CXXFLAGS は CFLAGS の中にある全てのフラグが設定されています. 大部分のシステムはこのように設定されるべきです. 通常のケースでは、 CXXFLAGS だけに追加のオプションを指定するようなことはほとんどありませんし、大体そういったオプションはグローバルに適用すべきではありません.

よくある誤解
CFLAGS でコンパイラの最適化をするのはより小さく高速なバイナリを生成するには効果的な方法ですが、一方で正しく動作しない、サイズの巨大化や動作速度の低下を引き起こしたり、そもそもコンパイルできないなどの問題を起こす可能性があります. CFLAGS を弄っているとこうしたことはあっさりと発生します. 適当に設定してはいけません.

にて設定した CFLAGS はシステム上の全てのパッケージに適用されます. そのため管理者は至極一般的で広く適用しても問題ないオプションのみを設定するのが普通です. これに加えて各々のパッケージがebuildやコンパイル中にさらに必要なオプションを自動で追加しています.

準備はできましたか？
リスクを伴うことを理解したところで、良識的、かつ安全な最適化の方法を見ていきましょう. そうすれば、この先Bugzillaで開発者に歓迎され、役立つ報告をすることができます. （開発者は、大抵、問題が再現するか確かめるために、最小限の CFLAGS でパッケージを再コンパイルすることを要求します. 挑戦的なフラグはコードを破壊しうることを覚えておいてください!）

基本
CFLAGS と CXXGLAGS の使用目的は、システム向けにあつらえた、可能な限り早くて小さな、かつ完全に動作するコードを生成することです. 時には、これらの条件は相互に排他的ですので、うまく動作すると分かっている組み合わせをここでは扱うことにします. 原則、それぞれのCPUアーキテクチャ向けに利用可能なよい最適化が用意されています. 参考情報として、さらにアグレッシブなフラグを後述します. このガイドではGCCのマニュアルに記載されているすべてのオプションを商会する訳ではありません. 基本的な、そして最も標準的なオプションについてのみ解説します.

-march
最初の、そしてもっとも重要なオプションは です. このオプションはコンパイラに対してどのシステムprocessor architecture|プロセッサアーキテクチャ (もしくは"arch")のためのコードを生成するのかを指示します. つまり、特定のCPU向けのコードを生成すべきであるといっているのです. CPUが違えば、性能が異なり、異なる命令セットをサポートし、コードの実行方法も違います. フラグは、あなたのCPUの全ての性能、機能、命令セット、癖などに合わせて特化したコードを生成するようにコンパイラに伝えます. 例えばAVX命令の恩恵を受けたいなら、ソースコードがそれに対応しておく必要があります.

はISA（訳註：命令セットアーキテクチャ）指定のオプションで、コンパイラにどんな命令が利用可能かを伝えます. Intel/AMD64プラットフォームで を使用すると、AVX命令はサポートされますがSSEのXMMレジスタがサポートされない場合があります. AVXのYMMレジスタを最大限利用したい場合、  または  も指定する必要があります.

は最適化オプション( と においてはデフォルト)で、可能であれば選択されたISAを使用してループのベクトル化を試みます. で有効化されていないのは、常にコードを改善するわけではなく、またコードを遅くしてしまう可能性があり、そしてたいていはコードを大きくしてしまうためです; 実際、これはループなどの状況次第です.

たとえに書いてある CHOST 変数を一般的なアーキテクチャに設定していても、 を設定すれば、プログラムは指定したプロセッサ向けに最適化されるでしょう. x86とx86-64のCPUは(とりわけ) フラグを使うべきです.

どんな種類のCPUを使っていますか？以下のコマンドを実行すれば、それが分かります.

あるいは、をインストールし、ツールがCPU_FLAGS_X86などの変数を通じて表示した利用可能なCPU特有のオプションをファイルに追加することもできます:

さらに と の値を含む詳細な情報を得たい場合は、以下の2つのコマンドが使えます.


 * 最初のコマンドはコンパイラにリンクを行わないようにさせ、また、 をコマンドラインオプションを明らかにせよとではなく、あるオプションが有効か無効かを示せという意味に解釈させます. ここでは、選択されたターゲットで有効なオプションが出力されます:


 * 二つ目のコマンドはヘッダーファイルをビルドしますが、実際には処理は行わずそれらを画面に表示することでコンパイラーディレクティブを表示します. 出力の最終行がすべての最適化オプションと選択されたアーキテクチャーを含むコマンドです:

では実際に を見てみましょう. この例は古いPentium IIIチップ向けです.

こちらはAMD64向けです.

CPUのタイプが決められない場合、またはどの設定を使えばいいかわからない場合、 を使うことができます. このフラグを使った場合、GCCはプロセッサを判別して、自動的にふさわしいフラグを設定するでしょう. しかしながら、このフラグは異なるCPU向けにパッケージをコンパイルする目的では使用すべきではありません！

例えば、あるコンピュータでパッケージをコンパイルして、しかしそれらを別のコンピュータで実行しようとしている場合 (処理の早いコンピュータで、古くて遅いマシンのためにビルドしているときなど)、 を使わないでください. "native"というのはコンパイルしているマシンのCPUタイプのみに特化して、アプリケーションのコードを生成することを意味しています. AMD Athlon 64上で と共にビルドされたアプリケーションは、古いVIA C3では実行することができないでしょう.

また、 と フラグも利用可能です. これらのフラグはたいてい フラグが利用できない場合にのみ使われます. 例えば特定のプロセッサアーキテクチャは や が必要になるかもしれません. 残念ながら、GCCの挙動はそれぞれのフラグの振る舞いが、あるアーキテクチャから近いアーキテクチャであってもあまり一貫性はありません.

x86とx86-64のCPUにおいて、 は利用可能な命令セットと正しいABIを使い、そのCPUに特化したコードを生成するでしょう. そのため古かったり種類の異なるCPUとの後方互換性は持っていません. i386やi486のような古いCPU向けにコードを生成する必要があるときのみ、 の使用を考慮するべきでしょう. は よりも一般的なコードを生成します. 特定のCPUコードにチューニングしますが、利用可能な命令セットやABIを考慮しないのです. はx86やx86-64のシステム上では非推奨となっているので、使わないでください.

x86/x86-64でない(SPARCやAlpha、PowerPCのような)CPUでのみ、 の代わりに や が必要になるでしょう. これらのアーキテクチャ上では、 / は(x86/x86-64上での) と同じように振る舞うでしょう･･･しかしフラグの名前は違うのです. 繰り返しますが、GCCの振る舞いとフラグ名はアーキテクチャを超えて一貫していないので、システムでどのフラグを使うべきなのかをGCCのマニュアルで必ず確認してください.

-O
次は フラグについてです. これは全体の最適化レベルをコントロールしますが、特にこの最適化レベルを上げることによって、ソースコードのコンパイルの時間がいくらか増えたり、よりたくさんのメモリを使用するようになります.

７つの 設定があります. 、 、 、 、 、 、 です. で指定できるのはこのうちの一つだけです.

を除いて、 の設定はいずれもいくつかの追加フラグを有効にします. なので、どの レベルで、どのフラグが有効になり、そのフラグにどんな効果があるのかを学ぶために、GCCマニュアルのoptimization options の章を読んで確認しましょう.

では、それぞれの最適化レベルを見てみましょう.


 * : このレベル("O"のあとにゼロが続いてます)は、完全に最適化をオフにします. CFLAGS や CXXFLAGS の中に が定義されていない場合のデフォルトです. このレベルはコンパイル時間を短縮して、生成するデバッグ情報を改善しますが、いくつかのアプリケーションは最適化がないと正しく動作しません. よって、デバッグ以外では推奨されません.


 * : これは最も基本的な最適化レベルです. コンパイラは時間をたくさんかけることなく、高速でサイズの小さなバイナリを生成しようと試みるでしょう. これは基本的な最適化しかおこないませんが、その代わり、いつでもうまくいくはずです.


 * : から更に踏み込みます. これは特別な理由がない限り"推奨される"最適化レベルです.  は で有効になるものに加え、さらにいくつかのフラグを有効にします.  を使うと、コンパイラはサイズとコンパイル時間に妥協せずにコードのパフォーマンスを改善しようと試みます. このレベルではSSEやAVXは利用されますが、YMMレジスタは も有効にしない限り使われません.


 * : これは取りうる最も高い最適化レベルです. コンパイル時間とメモリ使用量を犠牲にして最適化を実施します. ただし は性能を改善する保証がありません. 実際多くのケースで、バイナリサイズが大きくなり、メモリ使用量が増えることで逆にシステムが遅くなります. はいくつかのパッケージを壊すことがわかっています. これらの理由により は推奨されません. 一方、このレベルはコード内のループをベクトル化する を有効化し、AVX YMMレジスタも利用されます.


 * : このレベルはバイナリのサイズを重視して最適化するでしょう. これは フラグの中で、生成されるバイナリのサイズが増えないものを全て有効にします. CPUのキャッシュが小さかったり、ディスクの空き容量が極端に限られている場合などに非常に有効でしょう.


 * : GCC 4.8では新しい汎用的な最適化レベル が導入されました. このレベルはコンパイル時間の短縮とデバッグエクスペリエンスを向上させる一方、妥当なランタイム性能を確保することを目的としています. 結果的に開発全体で得られるものはデフォルトの最適化レベル より向上するはずです. と は同じでないことだけ理解しておいてください.  はデバッガとやりとりするために単純に最適化をオフにするだけです.


 * : GCC 4.7では、 に加えて 、 、 が利用できます. このオプションは規格への厳密な適合を犠牲にするため推奨されません.

前述の通り、 が推奨される最適化レベルです. もし 以外を使用してパッケージのコンパイルが失敗する場合は、 で再コンパイルしてみましょう. うまくいかなかった場合は、 CFLAGS と CXXFLAGS を(エラー報告や問題の調査向けに) や のように、最適化レベルを低く設定してみてください.

-pipe
よく使うフラグに があります. このフラグは、生成されるバイナリ自体には何の影響もありませんが、コンパイル時間が短縮されます. これはコンパイルにおける各処理の間で一時ファイルを使う代わりにパイプを使うように指示します. これにより多くのメモリを使うことになります. メモリに余裕のないシステムの場合、GCCが強制終了するかもしれません. そのような場合はこのフラグを使わないでください.

-fomit-frame-pointer
これは生成されるバイナリのサイズを減少させるために設計されているフラグで、よく利用されています. このフラグは、(x86-64のように)有効化してもデバッグ作業の阻害をしないアーキテクチャにおいては( を除く)全ての のレベルに対して有効になりますが、有効化する必要がある場合もあります. そのような場合にはこれをフラグに加えてください. GCCマニュアルによれば、全てのアーキテクチャにおいて、 を使えば が有効になる訳ではありません. GCC 4.6までは、もしくは を使っているときは、x86-32で有効化するためには明示的に オプションを使わなければなりません. ただし、 を使うとデバックは困難もしくは不可能になります.

特に、Javaで書かれgcjでコンパイルされたアプリケーションの問題解決をとても難しくします. もっとも、Javaだけがこのフラグの影響を受けるわけではありませんが. このように、このフラグは役立つ一方でデバッグを難しくしているのです. 特にバックトレースは役に立たなくなります. しかしながら、それほどソフトウェアのデバッグを行う予定がなく、他に のようなデバッグ関連の CFLAGS を追加していないのであれば、 を使ってみてもいいでしょう.

-msse、-msse2、-msse3、-mmmx、-m3dnow
これらのフラグはStreaming SIMD Extentions (SSE)、SSE2、SSE3、MMX、[https://ja.wikipedia.org/wiki/3DNow! 3DNow!]の命令セットをx86とx86-64アーキテクチャで有効にします. これらは主にマルチメディアやゲーム、その他の浮動小数点を多用する計算処理に向いています. その他にも有用な数学用機能の向上をいくつか含んでいます. 比較的新しいCPUならば、これらの命令セットに対応しています.

通常、正しい を使っている限り、これらのどのフラグもに加える必要はありません(例えば は を有効にします). いくつかの注意すべき例外は、比較的新しいVIAとAMD64のCPUです. VIAとAMD64はこれらの命令をサポートしますが、(SSE3のように) では有効になりません. これらのCPUについてはの出力を確認して、ふさわしいフラグを追加する必要があるでしょう.

-funroll-loopsや-fomg-optimizeを使ったら速くなったんだけど！
いいえ違います. フラグを付け加えれば付け加えるほど最適化されると言う誰かに騙されているだけです. システム全体で挑戦的なフラグを使うことはあなたのアプリケーションを傷つけるでしょう. GCC マニュアルでは と を使うとバイナリは大きくなり、実行も遅くなると述べています. またいくつかの理由から、これらの二つのフラグと同時に、 や や などの似たようなフラグが、速度を最大限誇示したい人たちの間で、とても人気を博しています.

ここで本当に問題なのは、これらのフラグは危険なほどに挑戦的なフラグということです. それらのフラグが何をやらかしているのかGentoo ForumsとBugzillaあたりをよく見てください. ろくなことないですよ！

それらのフラグを CFLAGS や CXXFLAGS に設定し、システム全体で使う必要はありません. それらはパフォーマンスに悪影響を及ぼすだけでしょう. それらのフラグが、最先端でハイパフォーマンスなシステムを使っているかのように見せるかもしれませんが、しかしそれらは何の効果もないどころか、バイナリのサイズが膨れ上がり、無効(INVALID)や修正の必要無し(WONTFIX)と結論づけられたバグを踏むことになります.

そのような危険なフラグを使う必要はありません. いえ、'''使わないでください. ''' 、 、 という基本を守り通してください.

3より高い-Oレベルはどう？
何人かのユーザーが、 や などを使うことによってもっといいパフォーマンスを得たと誇張していますが、3より高い レベルは何の効果もありません. コンパイラは のような CFLAGS も許容するでしょうが、それらは実質何もしないのです. の最適化を行うだけで、それ以上の最適化はしません.

さらに証拠が必要ですか？ソースコードを試してみてください.

見てのとおり、3より高いレベルであっても、結局 として扱われます.

実際のマシンと別のマシンでコンパイルするのはどう？
何人かの読者は、あきらかに劣ったCPUやGCCサブアーキテクチャでコンパイルすることを避けるために他のマシンでコンパイルすることは、（ネイティブな環境でのコンパイルと比較して）劣った最適化になるのか知りたくなるでしょう. 答えは単純でいいえです. コンパイルが走る実際のマシンに関係なく、またGCCをビルドしたときのCHOSTに関係なく、同じ引数が使用されている限り ( は除く) 、そしてGCCのバージョンが同じである限り (マイナーバージョンが違うかもしれません) 、最適化の結果は厳密に同じです.

例を一つあげます. GCCのCHOSTがi686-pc-linux-gnuとなっているマシンにGentooをインストールして、CHOSTがi486-linux-gnuとなっている別のPCにDistccサーバーをインストールします. リモートサーバーのコンパイラのサブアーキテクチャもしくはハードウェアが明らかに劣っている場合、最適化が十分なされないのではと心配する必要はありません. 結果は、両方のコンパイラに同じ引数が与えられている限り（かつ に が与えられていない限り）、ネイティブにビルドしたときと同じ最適化がかかります. ただしこの特殊なケースではDistcc and -march=nativeで説明されている通り、ターゲットのアーキテクチャが明示的に指定されなければなりません.

異なるサブアーキテクチャに向けてビルドされた２つのGCCの動作には一つしか違いがありません. それは暗に与えられるデフォルトの です. コマンドラインで を明示的に指定しなかった場合、GCCのCHOSTに設定された値が使用されます.

冗長なフラグ指定はどう？
しばしば、の中で、個々の レベルを指定すれば有効になるフラグを重複して CFLAGS や CXXFLAGS に設定していることがあります. これは時々知らずにやってしまうのですが、一方で(ebuildが行う)フラグの除去や置換を回避するために意図的に行われることがあります.

フラグの除去や置換はPortageツリーの中にある多くのebuildで行われます. 大抵は、特定の レベルでパッケージをコンパイルすると失敗するために、もしくはフラグを追加するとそのソースコードでは問題が出るためです. ebuildはどちらの場合も、全部/一部の CFLAGS と CXXFLAGS を除外するか、もしくは異なる レベルに置換するでしょう.

Gentoo Developer Manualにフラグの除去と置換がどのような場合に、どのように使われているのか概要が記載されています.

特定のレベルに対して重複してフラグを設定することによって、 に対するフラグ除去をある程度回避することができます. 例えば、 であれば次のようにします.

しかしながら、これは賢いやり方ではありません. CFLAGS は理由があって除去されるのです！ フラグが除去されるとき、それらのフラグでパッケージをビルドすると安全でないことを意味します. 明らかに分かっているのは、 でシステム全体をコンパイルすることは"安全ではない"という事です. そうすると、 の最適化で有効になるいくつかのフラグによって問題となるパッケージが出てくるでしょう. そのため、それらのパッケージをメンテナンスしている開発者の"裏をかく"ことを試みないでください. ''開発者を信用してください. ''フラグの除去と置換はシステムやアプリケーションの安定性を確保するために行われているのです！ もしebuildに代替のフラグが指定されているなら、それを回避しようとしないでください.

開発者が許可していないフラグでパッケージをビルドすれば、大概は数々の問題に繋がるでしょう. Bugzillaに不具合を報告する際には、で使っているフラグは容易く見抜かれてしまうので、余計なフラグを除いて再コンパイルする様に開発者に指示されるでしょう. 初めから冗長なフラグを指定しないことで、再コンパイルする手間を省いてください！ あなたが開発者よりよく知っていると根拠なく無意識に決めつけないでください.

LDFLAGSはどう？
Gentoo開発者がすでに基本的で安全な LDFLAGS を基本プロファイルにセットしているので、それらを変更する必要はありません.

パッケージごとにフラグを変更出来るの？
パッケージごとに( CFLAGS を含む)環境変数を変更する方法は、Gentoo Handbook, "パッケージごとの環境変数"で説明しています.

参考

 * コンパイルオプションを設定する (AMD64 ハンドブック)

外部資料
以下、最適化について理解を深めるための資料を紹介します.


 * GCC online documentation




 * ウィキペディア


 * Gentooフォーラム