web-dev-qa-db-ja.com

LinuxはGCC最適化なしではコンパイルできません。含意?

このようなインターネット上のいくつかのスレッドを見つけることができます:

http://www.gossamer-threads.com/lists/linux/kernel/972619

人々が-O0でLinuxをビルドできないと不平を言い、これはサポートされていないと言われています。 Linuxは、GCC最適化に依存して、関数を自動インライン化し、デッドコードを削除します。それ以外の場合は、ビルドを成功させるために必要なことを行います。

3.xカーネルの少なくとも一部については、これを自分で確認しました。 -O0でコンパイルした場合、ビルド時間の数秒後に私が試したものは終了します。

これは一般的に許容できるコーディング方法と考えられていますか?自動インライン化などのコンパイラー最適化は、信頼できるほど予測可能ですか。少なくとも1つのコンパイラのみを扱う場合はどうでしょうか。 GCCの将来のバージョンで、現在のLinuxカーネルのビルドがデフォルトの最適化(つまり、-O2または-Os)で破壊される可能性はどのくらいありますか?

そして、より注意深いメモ:3.xカーネルは最適化なしではコンパイルできないので、技術的に正しくないCコードと見なされるべきでしょうか?

9
DanL4096

複数の異なる(ただし関連する)質問を組み合わせました。それらのいくつかは、ここでは実際に話題になっていないため(たとえば、コーディング標準)、それらは無視します。

カーネルが「技術的に正しくないCコード」である場合、私はまず始めます。答えはカーネルが占める特別な位置を説明しているので、ここから始めます。それは、残りを理解するために重要です。

カーネルは技術的に不正なCコードですか?

答えは間違いなく「正しくない」です。

Cプログラムが正しくないと言える方法はいくつかあります。最初にいくつかの単純なものを邪魔にならないようにしましょう:

  • C構文に従っていない(つまり、構文エラーがある)プログラムは正しくありません。カーネルはC構文のさまざまなGNU=拡張機能を使用します。これらは、C標準に関する限り、構文エラーです(もちろん、GCCではそうではありません。-std=c99 -pedanticでコンパイルしてみてくださいまたは類似...)
  • 設計どおりの動作を行わないプログラムは正しくありません。カーネルは巨大なプログラムであり、変更ログの簡単なチェックでさえ証明されるように、確かにそうではありません。または、私たちがよく言うように、バグがあります。

Cでの最適化の意味

[注:このセクションには、実際のルールの非常に失われた言い直しが含まれています。詳細については、標準を参照し、スタックオーバーフローを検索してください。]

もう少し説明が必要な方のために。 C標準では、特定のコードは特定の動作を生成する必要があると述べています。また、構文的に有効なCには「未定義の動作」があるものも含まれています。 (残念ながら一般的です!)例は、配列の最後(バッファオーバーフローなど)を超えてアクセスすることです。

未定義の動作は強力です。プログラムにそれが含まれている場合でも、C規格は、プログラムが示す動作や、コンパイラーが直面したときにコンパイラーが生成する出力を気にしません。

しかし、プログラムに定義された動作のみが含まれている場合でも、Cではコンパイラにかなりの余裕が与えられています。ささいな例として(注:私の例では、簡潔にするために#include行などは省略しています):

void f() {
    int *i = malloc(sizeof(int));
    *i = 3;
    *i += 2;
    printf("%i\n", *i);
    free(i);
}

もちろん、5に続けて改行を出力します。それがC標準で要求されていることです。

そのプログラムをコンパイルして出力を逆アセンブルすると、メモリを取得するためにmallocが呼び出され、ポインタがどこかに格納され(おそらくレジスタ)、値3がそのメモリに格納され、次に2がそのメモリに追加される(多分)ロード、追加、保存が必要な場合も)、メモリはスタックにコピーされ、ポイント文字列"%i\n"がスタックに配置され、printf関数が呼び出されます。かなりの作業。しかし、代わりに、次のように表示されます。

/* Note that isn't hypothetical; gcc 4.9 at -O1 or higher does this. */
void f() { printf("%i\n", 5) }

そして、これが問題です。C標準ではそれが可能です。 C標準ではresultsのみが考慮され、達成方法は考慮されません。

それがCでの最適化です。コンパイラーは、C規格で要求される結果を達成するために、よりスマートな(通常はフラグに応じて、より小さくまたはより速く)方法を考え出します。 GCCの-ffast-mathオプションなど、いくつかの例外がありますが、それ以外の場合、最適化レベルは技術的に正しいプログラム(つまり、定義された動作のみを含むプログラム)の動作を変更しません。

定義された動作のみを使用してカーネルを作成できますか?

引き続きサンプルプログラムを見てみましょう。私たちが書いたバージョンであり、コンパイラーがそれを使用したバージョンではありません。まず、mallocを呼び出してメモリを取得します。 C標準は、mallocが何をするかを教えてくれますが、それをどのように行うかは教えていません。

(速度ではなく)明確にすることを目的としたmallocの実装を見ると、システムコール(mmap with MAP_ANONYMOUSなど)によって大きなチャンクが取得されていることがわかります。メモリ。内部的には、一部のデータ構造を保持して、そのチャンクのどの部分が使用されているか、解放されているかを伝えます。それは少なくともあなたが要求したものと同じ大きさの空きチャンクを見つけ、あなたが要求した量を切り分け、そしてそれへのポインタを返します。また、完全にCで記述されており、定義された動作のみが含まれています。スレッドセーフの場合、いくつかのpthread呼び出しが含まれている可能性があります。

最後に、mmapの機能を見ると、あらゆる種類の興味深いものがわかります。最初に、システムに十分な空きがあるかどうかを確認するためにいくつかのチェックを行いますRAMおよび/またはマッピングにスワップします。次に、ブロックを配置するための空きアドレススペースを見つけます。次に、データ構造はページテーブルと呼ばれ、おそらく途中で一連のインラインアセンブリ呼び出しを行います。実際には、物理​​メモリのいくつかの空きページ(つまり、実際のDRAMモジュールの実際のビット)を見つける可能性があります---他のプロセスを強制する必要があるプロセス同様に、要求されたブロック全体に対してそれを行わない場合は、そのメモリが最初にアクセスされたときに発生するように設定されます。これの多くは、インラインアセンブリ、さまざまな魔法のアドレスへの書き込みなど。特にスワップが必要な場合は、カーネルの大部分も使用することに注意してください。

インラインアセンブリ、マジックアドレスへの書き込みなどはすべてC仕様の範囲外です。これは驚くべきことではありません。 Cは、Cが発明された1970年代初頭にはほとんど想像もできなかったような多くの異なるマシンアーキテクチャで実行されます。マシン固有のコードを隠すことは、カーネル(およびある程度Cライブラリ)のコア部分です。

もちろん、例のプログラムに戻ると、printfが類似している必要があることが明らかになります。標準Cですべての書式設定などを行う方法はかなり明確です。しかし、実際にそれをモニターに表示しますか?または別のプログラムにパイプしますか?繰り返しになりますが、カーネル(およびおそらくX11またはWayland)によって行われる多くの魔法です。

カーネルが行う他のことを考えると、それらの多くはCの外部にあります。たとえば、カーネルはディスクからデータを読み取り(Cはディスク、PCIeバス、またはSATAを認識せず)、物理メモリ(Cはmallocのみを認識します) DIMM、MMUなどではなく)、それを実行可能にして(Cはプロセッサ実行ビットについて何も知らない)、関数として呼び出します(Cの外部だけでなく、非常に許可されていません)。

カーネルとそのコンパイラの関係

以前から覚えていて、プログラムに未定義の動作が含まれている場合、C標準に関する限り、すべての賭けは無効です。しかし、カーネルには実際には未定義の動作が含まれている必要があります。したがって、カーネルとそのコンパイラの間には何らかの関係がなければなりません。少なくとも、カーネル開発者は、C標準に違反してもカーネルが機能することを確信できるほどです。少なくともLinuxの場合、これにはカーネルがGCCが内部でどのように動作するかについてある程度の知識を持つことが含まれます。

壊れる可能性はどのくらいありますか?

将来のGCCバージョンはおそらくカーネルを壊すでしょう。これは数回前に起こったので、かなり自信を持って言うことができます。もちろん、GCCの厳密なエイリアシング最適化のようなものは、カーネル以外にも多くのことを壊しました。

Linuxカーネルが依存しているインライン化は自動インライン化ではなく、カーネル開発者が手動で指定したインライン化であることにも注意してください。 -O0を使用してカーネルをコンパイルし、いくつかの小さな問題を修正した後、カーネルが基本的に機能することを報告したさまざまな人がいます。 (1つは、リンク先のスレッドにもあります)。ほとんどの場合、カーネル開発者は-O0でコンパイルする理由がなく、副作用として最適化を必要とすることでいくつかのトリックが機能し、-O0でテストする人がいないため、サポートされていません。

例として、これは-O1以上でコンパイルおよびリンクしますが、-O0ではリンクしません。

void f();

int main() {
    int x = 0, *y;
    y = &x;

    if (*y)
        f();
    return 0;
}

最適化により、gccはf()が呼び出されないことを把握し、省略できます。最適化を行わないと、gccは呼び出しを残し、f()の定義がないため、リンカーは失敗します。カーネル開発者は、同様の動作に依存して、カーネルコードを読み書きしやすくしています。

13
derobert

Gentoo GCC Optimization Wiki から

セクション2.3:-Oフラグ

-O次は-O変数です。これは、最適化の全体的なレベルを制御します。これにより、コードのコンパイルにいくらか時間がかかり、特に最適化のレベルを上げると、より多くのメモリを消費する可能性があります。

-O設定には、-O0、-O1、-O2、-O3、-Os、-Og、-Ofastの7つがあります。 /etc/portage/make.confでそれらの1つだけを使用する必要があります。

-O0を除いて、-O設定はそれぞれいくつかの追加のフラグをアクティブ化するため、最適化オプションに関するGCCマニュアルの章を読んで、各-Oレベルでアクティブ化されるフラグと、それらのフラグに関する説明を必ず確認してください。行う。

各最適化レベルを調べてみましょう:

-O0:このレベル(文字「O」の後にゼロが続く)は、最適化を完全にオフにし、CFLAGSまたはCXXFLAGSで-Oレベルが指定されていない場合のデフォルトです。これにより、コンパイル時間が短縮され、デバッグ情報が改善されますが、一部のアプリケーションは、最適化を有効にしないと正しく動作しません。このオプションは、デバッグ以外の目的にはお勧めできません。
-O1:これは最も基本的な最適化レベルです。コンパイラーは、コンパイルに時間をかけずに、より高速で小さなコードを生成しようとします。それはかなり基本的ですが、それは仕事をいつも終わらせるはずです。
-O2:-O1からのステップアップ。これは、特別な必要がない限り、最適化の推奨レベルです。 -O2は、-O1によってアクティブ化されるフラグに加えて、さらにいくつかのフラグをアクティブ化します。 -O2を使用すると、コンパイラーは、サイズに妥協することなく、またコンパイルに時間をかけすぎずに、コードのパフォーマンスを向上させようとします。
-O3:これは可能な最高レベルの最適化です。コンパイル時間とメモリ使用量の面でコストのかかる最適化を可能にします。 -O3を使用したコンパイルは、パフォーマンスを向上させるための保証された方法ではありません。実際、多くの場合、バイナリが大きくなり、メモリ使用量が増えるため、システムの速度が低下する可能性があります。 -O3はいくつかのパッケージを壊すことも知られています。したがって、-O3の使用はお勧めしません。
-Os:このオプションは、コードのサイズを最適化します。生成されたコードのサイズを増加させないすべての-O2オプションをアクティブにします。これは、ディスクストレージスペースが非常に限られているマシンや、CPUのキャッシュサイズが小さいマシンに役立ちます。
-Og:GCC 4.8では、新しい一般的な最適化レベル-Ogが導入されました。適度なレベルのランタイムパフォーマンスを提供しながら、高速コンパイルと優れたデバッグエクスペリエンスのニーズに対応します。開発の全体的なエクスペリエンスは、デフォルトの最適化レベル-O0よりも優れています。 -Ogは-gを意味するのではなく、単にデバッグを妨げる可能性のある最適化を無効にすることに注意してください。
-Ofast:GCC 4.7の新機能で、-O3と-ffast-math、-fno-protect-parens、および-fstack-arraysで構成されています。このオプションは、厳密な標準への準拠に違反するため、使用は推奨されません。前述のように、-O2は推奨される最適化レベルです。パッケージのコンパイルが失敗し、-O2を使用していない場合は、そのオプションを使用して再構築してください。フォールバックオプションとして、CFLAGSとCXXFLAGSを-O1または-O0 -g2 -ggdb(エラー報告と考えられる問題のチェック用)などのより低い最適化レベルに設定してみてください。

特に-O0について質問しましたが、これは最適化ではありません。上記を読むと、O0はデバッグにのみ使用する必要があることが示されます。 menuconfigを使用したことがある場合は、カーネルデバッグを有効または無効にするオプションがあることに気づくでしょう。このオプションを有効にすると、O0が情報を提供するのとほぼ同じ方法でデバッグ情報が出力されます。また、システム全体が1つだけの最適化設定でビルドまたはコンパイルされている、つまり、O0でカーネルをコンパイルできず、O2でシステムの残りの部分をコンパイルできないという点が不足していると思います。


1つのバージョンで-Oフラグを有効にすることは新しいバージョンの-O設定と同じであるため、GCCバージョン間の下位互換性については、GCCは常にバージョン間で互換性を保ちます。 GCC4.7と-Ofastオプションに関する上記の注記を参照してください。このオプションは4.7以降でのみ使用できますが、4.7の-O2 =すべてのバージョンで-O2

0
eyoung100