GCC と Clang を使用した C/C++ バイナリの最適化

最終更新: 14/01/2026
  • C/C++における優れた最適化の基本は、賢く組み合わせることである。 -marchレベル -O そして、安全な選択肢としては -pipe.
  • LTO、PGO、OpenMP、Graphite などの高度な技術を使用すると大幅な改善が実現できますが、コンパイルとデバッグの複雑さが増します。
  • 強化フラグ (FORTIFY、スタック プロテクター、PIE、relro/now) は、パフォーマンスの低下と引き換えにセキュリティを強化します。
  • CMake とさまざまなジェネレーターを使用すると、ソース コードに触れることなく、GCC、Clang、MSVC などのさまざまなプラットフォーム間で移植可能なコードを維持できます。

GCC と Clang による C および C++ バイナリの最適化

C言語やC++言語でコンパイルオプションを試し始めると、オンラインで見かける「便利な」フラグをすべて有効にしたくなる誘惑に駆られるかもしれません。しかし実際には、パラメータの組み合わせを誤ると、システムが不安定になったり、コンパイルが失敗したり、さらに悪いことに、非常に微妙な問題で失敗するバイナリが生成されたり、情報抽出が必要になったりする可能性があります。このような場合、調査のためにバイナリから隠されたテキストを抽出することが役立つことがあります。

このガイドの目的は、実践的かつ分かりやすく、 GCC と Clang を使用した C/C++ バイナリの最適化 正しいオプションを使用する:古典的なものから -O2, -march y -pipeLTO、PGO、OpenMP、Graphite、セキュリティ強化といった高度な技術まで、幅広い分野を網羅しています。さらに、CMake、MinGW/MSYS2、Visual Studio、Xcode、Ninjaとこれらの技術を組み合わせ、移植性とメンテナンス性に優れた環境を構築する方法も学びます。

CFLAGS と CXXFLAGS とは何ですか? また、混乱を招かずにこれらを使用するにはどうすればよいでしょうか?

ほぼすべての種類のシステムにおいて Unixの (Linux、BSDなど)変数が使用される CFLAGS y CXXFLAGS オプションを渡す CおよびC++コンパイラの仕様です。正式な標準規格ではありませんが、非常に一般的なため、よく書かれたビルドシステム(Make、Autotools、CMake、Mesonなど)はこれを遵守しています。

Gentooのようなディストリビューションでは、これらの変数はグローバルに定義されています。 /etc/portage/make.confそこから、Portageでコンパイルされたすべてのパッケージに継承されます。他のシステムでは、シェルでエクスポートしたり、… Makefileスクリプト CMake または類似のものから。

定義するのはよくあることです CXXFLAGS コンテンツの再利用 CFLAGS 必要に応じて、C++ 固有のオプションを追加します。例: CXXFLAGS="${CFLAGS} -fno-exceptions"重要なのは、そこに無差別にフラグを追加しないことです。なぜなら、それらのフラグはコンパイルするすべてのものに適用されてしまうからです。

CFLAGS/CXXFLAGSオプションを過度に設定すると、コンパイルが失敗したり、デバッグが非常に困難なバグが発生したり、バイナリの実行速度が低下したりする可能性があることを理解しておくことが重要です。高度な最適化が必ずしもパフォーマンス向上につながるとは限らず、一部の変換処理は、コードが満たしていない前提条件を悪用する可能性があります。

基本的な最適化: -march、-mtune、-O レベル

賢明な調整の基礎には、次の 3 つの要素が含まれます。 CPU アーキテクチャを選択し、最適化レベルを選択し、場合によっては小さな無害な拡張機能を有効にします。 として -pipeそれ以外のことは、ほとんどすべて、後で冷静な頭で考えるべきです。

アーキテクチャの選択: -march、-mtune、および company

選択 -march=<cpu> GCC/Clangに特定のプロセッサフ​​ァミリーを指示する 行きます コードを生成する特定の命令(SSE、AVX、AVX2、AVX-512など)の使用とABIの調整が可能です。あまりに新しいCPUを選んだ場合、古いマシンではバイナリが起動しなくなります。

Linuxでは、プロセッサが何をサポートしているかを調べるには、 /proc/cpuinfo または使用 コマンド スタイルコンパイラ自体から gcc -Q -O2 --help=target現代のx86-64システムでは、次のような汎用プロファイルが標準化されている。 x86-64-v2, x86-64-v3 y x86-64-v4このグループは命令セットを増やしており、GCC 11 以降でサポートされています。

プラス -march、存在する -mtune=<cpu> 計画を「微調整」する 新しい命令を使用せずにコードから特定のモデルへ変換する。x86以外のアーキテクチャにも存在する。 -mcpu y -mtune 関連するオプションには(ARM、PowerPC、SPARCなど)が含まれます。x86では、 -mcpu それは実際には時代遅れです。

よく使われるトリックは -march=nativeこれにより、コンパイラはローカルマシンのCPUを検出し、適切な拡張機能を自動的に有効化できます。これは、バイナリをコンパイルしたマシンでのみ実行する環境では理想的ですが、他のCPU向けのパッケージを生成する場合は致命的な罠となります。

最近のプロセッサでは インテル AMDでは、GCCは各ファミリーに固有の名前を組み込んでいます。 -march=rocketlake, -march=sapphirerapids, -march=znver2 o -march=znver3これらのオプションは、各世代の高度な命令(AVX2、AVX-512、FMAなど)をグループ化し、 ハードウェア どこに展開するかがわかっている場合。

最適化レベル -O: それぞれのレベルをいつ使用するか

選択 -O 最適化の全体的なレベルを制御する コードに適用されます。各ステップでは、より広範な変換セットがアクティブ化され、コンパイル時間とメモリ消費量、そしてデバッグの容易さの両方に影響を与えます。

  • -O0最適化なし。何も指定しない場合はこれがデフォルトです。コンパイル速度は速く、デバッグしやすいコードを生成しますが、遅く、サイズも大きくなります。開発初期段階や複雑なバグの調査に最適です。
  • -O1最適化の第 1 レベル。比較的低コストの改善を適用することで、通常はコンパイルを過度に重くすることなく、パフォーマンスを適度に向上させます。
  • -O2: ほとんどのプロジェクトでの一般的な使用に推奨されるレベルです。 パフォーマンス、コンパイル時間、安定性のバランスが良好です。そのため、多くのディストリビューションではこの値がデフォルトとして使用されます。
  • -O3: すべての最適化を有効化 -O2 より強力なループアンワインドや、より強力なベクトル化といった、より積極的な変換。これは一部の数値コードでは非常に効果的ですが、コード内のUB(未定義コード)が発見されたり、実行ファイルのサイズが肥大化したりする可能性が高くなります。
  • -Osこれは、速度よりもスペースを優先することでバイナリサイズを縮小します。これは、 ストレージ またはキャッシュが非常に限られている。
  • -Oz (GCC 12+): 大幅なパフォーマンス低下を許容しつつ、サイズを極限まで削減します。非常に小さなバイナリや非常に特殊なシナリオに有効です。
  • -Ofastそれはまるで -O3 C/C++標準に厳密に準拠しているわけではありません。特に浮動小数点演算において、言語の保証を破ることでパフォーマンスを向上させることが可能です。そのため、自分が何をしているのかを十分に理解した上で使用する必要があります。
  • -Ogデバッグ用に設計されています。デバッガにあまり影響を与えない最適化のみを適用し、コードを中間の状態に維持します。 -O0 y -O1.

上位レベル -O3 として -O4 o -O9 それらはすべて煙と鏡だコンパイラはこれらを受け入れますが、内部的には -O3そこに隠された魔法はなく、ただポーズをとっているだけです。

  Windows で Alt キーを使用して特殊文字を入力する方法

ビルドが不可解に失敗したり、奇妙なクラッシュが発生したり、最適化ツールによって結果が異なる場合、適切な診断手順は次のようになります。 一時的に下がる -O1 O incluso -O0 -g2 -ggdb 簡単にデバッグできるバイナリを入手し、有用な情報とともにバグを報告します。

-pipeとその他の基本オプション

-pipe コンパイラにメモリ内のパイプを使用するように指示します コンパイルフェーズ(プリプロセス、コンパイル、アセンブリ)間のディスク上の一時ファイルの代わりに使用します。これにより、通常は処理速度が多少向上しますが、RAMの消費量は増加します。メモリが非常に少ないマシンでは、コンパイラがクラッシュする可能性があるため、そのような場合は慎重に使用してください。

その他の伝統的な選択肢としては、 -fomit-frame-pointer これらのフラグを使用するとスタックポインタレジスタを解放してより多くのコードを生成できますが、クリーンなバックトレースによるデバッグが難しくなります。最近のx86-64アーキテクチャでは、コンパイラがこれを適切に処理するため、手動で設定する必要すらほとんどありません。

SIMD拡張、Graphite、ループベクトル化

x86-64用の最新のコンパイラは、選択されたCPUに応じて多くのSIMD命令を自動的に有効にします。 -marchそれでも、次のような旗が目に入るでしょう -msse2, -mavx2 または明示的に追加できる同様のもの。

一般的に、 -march これは適切です。手動でアクティブ化する必要はありません。 -msse, -msse2, -msse3, -mmmx o -m3dnowこれらは既にデフォルトで有効になっているためです。GCC/Clang がデフォルトで有効にしていない特定の CPU でのみ、強制的に有効にすることが理にかなっています。

複雑なループの場合、GCCには最適化のセットが含まれています グラファイトISLライブラリに依存しています。次のようなフラグを通じて -ftree-loop-linear, -floop-strip-mine y -floop-block コンパイラはループを分析し、データの局所性と並列性を向上させるためにループを再構成することができます。具体的なケースについては、 低レベルCの例 これらの変換にコードを適応させるのに役立ちます。

これらの変換は、複雑な数値計算コードにおいて良好な結果をもたらす可能性がありますが、無害ではありません。コンパイル時のRAM消費量を大幅に増加させたり、これらの変換を念頭に置いていない大規模プロジェクトでエラーを引き起こしたりする可能性があります。したがって、これらの変換は、テスト済みで正しく動作することが証明されている特定のコードまたはプロジェクトでのみ有効にすることをお勧めします。

並列処理: OpenMP、-fopenmp、-ftree-parallelize-loops

コードで OpenmpGCCとClangはどちらもオプションを通じてかなりしっかりしたサポートを提供している -fopenmpこれにより、ソース コード自体のディレクティブを使用して、コードのセクション、特にループを並列化し、コンパイラが複数のスレッドで作業を生成できるようになります。

プラス -fopenmpGCCにはオプションが含まれています -ftree-parallelize-loops=Nどこで N 通常は利用可能なコアの数に設定されます(たとえば、 $(nproc) (ビルドスクリプト内)。これにより、手動でディレクティブを追加することなくループを自動的に並列化しようとしますが、成功の度合いはコードの書き方に大きく依存します。

  最もよく使用される 6 つのファイル形式

心に留めておきます システム全体でOpenMPをグローバルに有効にすることは非常に問題になる可能性がある一部のプロジェクトではこれに対する準備ができていませんが、他のプロジェクトでは独自の並行性モデルを使用しており、また、これに遭遇するとコンパイルに失敗するプロジェクトもあります。 -fopenmp賢明な方法は、システムのグローバル CFLAGS ではなく、プロジェクトごと、あるいはモジュールごとに有効にすることです。

リンクタイム最適化: LTO

リンク時最適化(LTO)により、コンパイラは最適化を行う際に単一のソースファイルに限定されることなく、リンク段階でプログラム全体を参照し、関連するすべてのオブジェクトに対してグローバルな最適化を適用できるようになります。

GCCでは次のように有効化されます。 -fltoスレッドの数も指定できる。例えば -flto=4、またはコアの数を検出して -flto=autoこれも使用される場合 -fuse-linker-plugin リンカーとともに ゴールド また、binutils に LTO プラグインをインストールすると、コンパイラはバインディングに関係する静的ライブラリからでも LTO 情報を抽出できます。

LTOは、不要なコードを削除し、モジュール間のインライン化を可能にするため、一般的に実行可能ファイルのサイズが小さく、多くの場合、実行速度も速くなります。しかし、コンパイル時間とメモリ消費量は大幅に増加し、特に数千ものオブジェクトファイルを含む大規模プロジェクトでは顕著です。

Gentooのようにシステム全体をソースから再コンパイルする環境では、LTOをグローバルに適用することは依然としてリスクが高いと考えられています。多くのパッケージはまだLTOとうまく連携せず、選択的に無効化する必要があるためです。したがって、一般的には、効果が顕著に現れる特定のプロジェクトやGCC/Clangビルドでのみ有効にすることが推奨されます。

PGO: プロファイルガイド最適化

プロファイル誘導型最適化(PGO)とは、計測機能を備えたプログラムを一度コンパイルし、代表的なワークロードで実行してパフォーマンス統計を収集し、その後、収集したプロファイルを使用して最適化ツールを誘導するためにプログラムを再コンパイルする手法である。

GCC での一般的なフローは次のようになります。 まずコンパイルする -fprofile-generateプログラム(またはそのテスト)を実行してプロファイルデータを生成し、 コンパイルする -fprofile-use プロファイルファイルが保存されているディレクトリを指します。追加オプションとして、 -fprofile-correction または特定の通知を無効にすることで(-Wno-error=coverage-mismatch)フェーズ間のコード変更によって頻繁に発生するエラーを回避できます。また、通常は eBPFとperfでパフォーマンスを監視する 正確なプロファイルを取得します。

適切に実装されたPGOは 単にレベルを上げるよりもはるかに大きなパフォーマンス向上をもたらします -O汎用的なモデルではなく、実際の実行データに基づいて判断を行うためです。問題は、このプロセスが煩雑であることです。関連するコードの更新ごとに繰り返す必要があり、テストシナリオが実際の使用状況を反映していることに大きく依存します。

一部のプロジェクト(特定のディストリビューションにおけるGCC自体を含む)では、 PGOを自動的に有効化するための特定のフラグやスクリプトが既に提供されていますが、一般的には、このプロセスに時間を費やす意欲のある上級ユーザー向けの技術となっています。

強化:フラグベースのセキュリティ

速度以外にも、多くの環境では、多少のパフォーマンス低下を犠牲にしてでも、バイナリの脆弱性対策を強化することに重点を置いています。GCCや最新のリンカーは、CFLAGS/CXXFLAGSおよびLDFLAGSから有効化できる、幅広いセキュリティ強化オプションを提供しています。

最も一般的な例としては、以下のようなものがあります。

  • -D_FORTIFY_SOURCE=2 o =3: 実行時にバッファ オーバーフローを検出するために、特定の libc 関数に追加のチェックを追加します。
  • -D_GLIBCXX_ASSERTIONS: STL 内のコンテナーと C++ 文字列の境界チェックをアクティブ化し、範囲外のアクセスを検出します。
  • -fstack-protector-strong: スタックにカナリアを挿入して、スタックを破壊する書き込みを検出します。
  • -fstack-clash-protection: スタックと他のメモリ領域間の衝突に基づいて攻撃を軽減します。
  • -fcf-protection: 対応するアーキテクチャに制御フロー保護 (ROP 攻撃などに対する保護) を追加します。
  • -fpie ととも​​に -Wl,-pie: 効果的な ASLR に必要な、配置可能な実行可能ファイルを生成します。
  • -Wl,-z,relro y -Wl,-z,now再配置テーブルを強化し、遅延バインディングを無効にします。 シンボル特定の攻撃ベクトルを妨害します。
  アプリケーション セキュリティ ポスチャ管理 (ASPM) とは何ですか? また、アプリケーションのセキュリティをどのように向上させるのですか?

一部のディストリビューションの「強化」プロファイルでは、これらのオプションの多くがデフォルトで有効になっています。影響を理解せずに手動で有効にすると、特に大規模なアプリケーションやメモリを大量に消費するアプリケーションでは、バイナリの実行速度が著しく低下する可能性がありますが、公開されているサーバーや機密性の高いデスクトップ環境では、通常は妥当なトレードオフとなります。

コンパイラと環境を選択します: GCC、Clang、MSVC、MinGW、Xcode…

実際には、フラグだけでなく、各プラットフォームで使用するコンパイラやツールチェーン全体を選択することもよくあります。GCCとClangは通常、パフォーマンスが非常に似ており、違いは診断機能、コンパイル時間、特定の拡張機能との互換性などでより顕著になります。

En Windows いくつかのルートがあります: ビジュアルスタジオ(MSVC) ツールセットで v143, v142等; または MinGW-w64 スルー MSYS2 これにより、ネイティブのWindows GCCとClangに加え、必要なWin32ライブラリが提供されます。MSYS2は、 pacman また、MinGW64 環境 (従来の MSVCRT に基づく) と UCRT64 (Universal CRT を使用した、より新しい) も提供します。

macOS では、標準的なアプローチはXcodeと clang/clang++ を使用することです。ここで重要な概念は、ベース SDK(コンパイル対象のシステム バージョン)とデプロイメント ターゲット(アプリを実行したい最小 macOS バージョン)です。これら 2 つの設定を正しく合わせることで、最新のシステム バージョンのみをコンパイルしてしまい、バイナリが少し古いバージョンで実行できなくなるという、よくある問題を回避できます。

Linuxでは、通常は GCCとMakeまたはNinjaCMakeをメタジェネレータとして使うのもいいかもしれません。さらに、Ubuntuのようなディストリビューションでは、複数のバージョンのGCCをインストールして、 update-alternativesmacOSでの使い方と同様 xcode-select Xcode から切り替えます。

MakeやNinjaで生成されたプロジェクト(これらは単一構成)の便利なデバッグ環境が必要な場合は、Eclipse CDTVisual Studio Codeが非常に便利な選択肢となります。CMakeは必要なプロジェクトファイルを生成したり、それらと直接統合して構成、コンパイル、デバッグを行うことができます。

移植性とCMake: 同じコード、異なるツールチェーン

Windows、Linux、macOS 上のコードに触れることなく C/C++ プロジェクトをコンパイルするには、両方の適切な組み合わせが必要です。 CMake、利用可能なジェネレータ、そしてさまざまなコンパイラアイデアとしては、ファイルは CMakeLists.txt プロジェクトを抽象的に記述すると、CMake は各プラットフォームに適切なプロジェクト タイプを生成します。

WindowsではCMakeを次のように呼び出すことができます。 -G "Visual Studio 17 2022" msbuildでソリューションを作成するか、 -G "Ninja" コンソールからのビルドを高速化します。さらに、 -T v143, v142など、プラットフォームツールセット(MSVCコンパイラバージョン)を選択し、 -A x64, Win32 o arm64 アーキテクチャを選択します。

MinGW/MSYS2では通常、 -G "MinGW Makefiles" o -G "Ninja" そして、変数を通して CMAKE_C_COMPILER y CMAKE_CXX_COMPILERGCCかClangのどちらを使用するかを選択します。この場合、設定(デバッグ、リリースなど)は以下から制御されます。 -DCMAKE_BUILD_TYPEMake と Ninja は単一構成であるためです。

macOSでは、 -G Xcode IDEでデバッグするのに最適なプロジェクトを提供し、次のような変数でSDKとデプロイメントターゲットを制御できます。 CMAKE_OSX_DEPLOYMENT_TARGETMake または Ninja だけが必要な場合は、Linux と同じジェネレータを使用します。

この方法の素晴らしい点は、適切に設定すれば、単一のコードベースと一貫したフラグセット(プラットフォーム固有の場合もある)を維持し、ソースコードを絶えずいじることなく、あらゆる環境でコンパイルできることです。ただし、重要な原則を忘れてはいけません。まず、きちんと動作することを確認し、それから最適化を進めていくのです

見てきたすべてのことを踏まえると、一般的な考え方は 適度だが効果的な組み合わせ (こんな感じ) -O2 -march=<cpu adecuada> -pipe 加えて、ある程度の妥当な強化を行い、LTO、PGO、Graphite、積極的な OpenMP などの強力なツールは、改善が確実に測定され、それによってもたらされる保守およびデバッグのコストが許容されるプロジェクトまたはモジュール用に確保しておきます。

eBPFとbpftraceでパフォーマンスを監視する
関連記事:
Linux で eBPF、bpftrace、perf を使用してパフォーマンスを監視する