web-dev-qa-db-ja.com

なぜCコンパイラは構造体のメンバーを再配置して整列パディングを排除できないのですか?

可能性のある複製:
GCCが構造体を最適化しない理由
C++が構造を厳しくしないのはなぜですか

32ビットx86マシンでの次の例を検討してください。

アライメントの制約のため、次の構造体

struct s1 {
    char a;
    int b;
    char c;
    char d;
    char e;
}

メンバーが次のように並べ替えられた場合、よりメモリ効率の高い方法で表現できます(12対8バイト)

struct s2 {
    int b;
    char a;
    char c;
    char d;
    char e;
}

C/C++コンパイラはこれを許可されていないことを知っています。私の質問は、言語がこのように設計された理由です。結局、膨大な量のメモリと、struct_ref->bは違いを気にしません。

[〜#〜] edit [〜#〜]:非常に有用な回答をありがとうございました。言語の設計方法が原因で再配置が機能しない理由を非常によく説明します。しかし、再配置が言語の一部である場合、これらの議論はまだ保持されるだろうかと思う。特定の再配置規則があり、そこから少なくともそれが必要だったとしましょう

  1. 実際に必要な場合にのみ構造体を再編成する必要があります(構造体が既に「タイト」である場合は何もしないでください)
  2. ルールは構造体の定義のみを参照し、内部構造体の内部は参照しません。これにより、構造体タイプは、別の構造体の内部にあるかどうかにかかわらず、同じレイアウトになります。
  3. 与えられた構造体のコンパイルされたメモリレイアウトは、その定義があれば予測可能です(つまり、ルールは固定されています)

あなたの議論を一つずつ解決する理由:

  • 低レベルデータマッピング、「最も驚きの要素」:構造体を(@Perryの答えのように)タイトなスタイルで記述するだけで、何も変更されていません(要件1)。何らかの奇妙な理由で内部パディングが必要な場合は、ダミー変数を使用して手動で挿入したり、キーワードやディレクティブがある可能性があります。

  • コンパイラの違い:要件3はこの懸念を排除します。実際、@ David Heffernanのコメントから、コンパイラが異なるとパッドが異なるため、今日この問題が発生しているようです。

  • 最適化:並べ替えのポイントは(メモリ)最適化です。ここには多くの可能性があります。パディングをすべて削除することはできないかもしれませんが、並べ替えによって最適化がどのように制限されるかはわかりません。

  • 型キャスト:これが最大の問題であるように思えます。それでも、これを回避する方法があるはずです。ルールは言語で固定されているため、コンパイラはメンバーがどのように並べ替えられたかを把握し、それに応じて反応することができます。前述のように、完全な制御が必要な場合には、常に並べ替えを防ぐことができます。また、要件2により、タイプセーフなコードが破損しないことが保証されます。

このような規則が理にかなっていると思うのは、構造体のメンバーをタイプよりもコンテンツでグループ化する方が自然だと思うからです。また、多くの内部構造体を持っているときよりも、コンパイラーが最適な順序を選択する方が簡単です。最適なレイアウトは、タイプセーフな方法で表現できないものですらあります。一方で、言語をより複雑にするように見えますが、これはもちろん欠点です。

言語の変更については話していないことに注意してください-異なる方法で設計できた場合に限ります。

私の質問は架空のものであることがわかっていますが、この議論は、機械と言語の設計の下位レベルでより深い洞察を提供すると思います。

私はここでかなり新しいので、これについて新しい質問をする必要があるかどうかはわかりません。その場合は教えてください。

87
Halle Knast

Cコンパイラがフィールドを自動的に並べ替えることができない理由は複数あります。

  • Cコンパイラは、structが現在のコンパイル単位(たとえば、外部ライブラリ、ディスク上のファイル、ネットワークデータ、CPUページテーブルなど)を超えるオブジェクトのメモリ構造を表すかどうかを知りません。そのような場合、データのバイナリ構造もコンパイラがアクセスできない場所で定義されるため、structフィールドを並べ替えると、他の定義と矛盾するデータ型が作成されます。たとえば、- Zipファイルのファイルのヘッダー には、複数の位置合わせされていない32ビットフィールドが含まれています。フィールドを並べ替えると、Cコードがヘッダーを直接読み書きできなくなります(Zip実装がデータに直接アクセスしたい場合):

    struct __attribute__((__packed__)) LocalFileHeader {
        uint32_t signature;
        uint16_t minVersion, flag, method, modTime, modDate;
        uint32_t crc32, compressedSize, uncompressedSize;
        uint16_t nameLength, extraLength;
    };
    

    packed属性は、コンパイラがフィールドを自然な配置に従って配置するのを防ぎ、フィールドの順序付けの問題とは関係ありません。 LocalFileHeaderのフィールドを並べ替えて、構造のサイズを最小にし、すべてのフィールドを自然な配置に揃えることができます。ただし、コンパイラーは、構造体が実際にZipファイル仕様で定義されていることを知らないため、フィールドを並べ替えることはできません。

  • Cは安全でない言語です。 Cコンパイラは、コンパイラが認識しているものとは異なる型を介してデータにアクセスするかどうかを知りません。たとえば、次のとおりです。

    struct S {
        char a;
        int b;
        char c;
    };
    
    struct S_head {
        char a;
    };
    
    struct S_ext {
        char a;
        int b;
        char c;
        int d;
        char e;
    };
    
    struct S s;
    struct S_head *head = (struct S_head*)&s;
    fn1(head);
    
    struct S_ext ext;
    struct S *sp = (struct S*)&ext;
    fn2(sp);
    

    これは、広く使用されている低レベルプログラミングパターンです。特に、ヘッダーにヘッダーのすぐ上にあるデータのタイプIDが含まれる場合。

  • struct型が別のstruct型に埋め込まれている場合、内側のstructをインライン化することはできません。

    struct S {
        char a;
        int b;
        char c, d, e;
    };
    
    struct T {
        char a;
        struct S s; // Cannot inline S into T, 's' has to be compact in memory
        char b;
    };
    

    これは、いくつかのフィールドをSから別の構造体に移動すると、いくつかの最適化が無効になることも意味します。

    // Cannot fully optimize S
    struct BC { int b; char c; };
    struct S {
        char a;
        struct BC bc;
        char d, e;
    };
    
  • ほとんどのCコンパイラはコンパイラを最適化しているため、構造体フィールドを並べ替えるには、新しい最適化を実装する必要があります。これらの最適化が、プログラマーが作成できるものよりも優れているかどうかは疑問です。データ構造を手動で設計することは、レジスタ割り当て、関数のインライン化、定数の折りたたみ、switchステートメントのバイナリへの変換などの他のコンパイラタスクよりも時間がかかりませんしたがって、コンパイラーがデータ構造を最適化できるようにすることで得られる利点は、従来のコンパイラーの最適化ほど具体的ではないようです。

70
user811773

Cは、移植性のないハードウェアを記述し、高レベル言語で依存コードをフォーマットできるようにすることを目的としています。プログラマーの背中の後ろの構造コンテンツの再配置は、その能力を破壊します。

NetBSDのip.hからのこの実際のコードを観察してください:


/*
 * Structure of an internet header, naked of options.
 */
struct ip {
#if BYTE_ORDER == LITTLE_ENDIAN
    unsigned int ip_hl:4,       /* header length */
             ip_v:4;        /* version */
#endif
#if BYTE_ORDER == BIG_ENDIAN
    unsigned int ip_v:4,        /* version */
             ip_hl:4;       /* header length */
#endif
    u_int8_t  ip_tos;       /* type of service */
    u_int16_t ip_len;       /* total length */
    u_int16_t ip_id;        /* identification */
    u_int16_t ip_off;       /* fragment offset field */
    u_int8_t  ip_ttl;       /* time to live */
    u_int8_t  ip_p;         /* protocol */
    u_int16_t ip_sum;       /* checksum */
    struct    in_addr ip_src, ip_dst; /* source and dest address */
} __packed;

この構造は、IPデータグラムのヘッダーとレイアウトが同じです。イーサネットコントローラによってIPデータグラムヘッダーとして消去されたメモリの塊を直接解釈するために使用されます。コンパイラが作者の下からコンテンツを勝手に再配置した場合を想像してみてください。これは災害になります。

そして、はい、それは正確に移植可能ではありません(そして__packedマクロを介してそこに与えられた移植不可能なgccディレクティブさえあります)、それはポイントではありません。 Cは具体的にはdesignedで、ハードウェアを駆動するための移植性のない高レベルコードを記述できるようにします。それが人生の機能です。

29
Perry

C [およびC++]はシステムプログラミング言語と見なされているため、ポインタを使用してメモリなどのハードウェアに低レベルでアクセスできます。プログラマは、データチャンクにアクセスして構造体にキャストし、さまざまなメンバーにアクセスすることができます[簡単に]。

別の例は、可変サイズのデータ​​を格納する以下のような構造体です。

struct {
  uint32_t data_size;
  uint8_t  data[1]; // this has to be the last member
} _vv_a;
11
perreal

WG14のメンバーではないので、決定的なことは何も言えませんが、自分のアイデアはあります。

  1. それは最も驚きの原則に違反します-最もスペース効率が高いかどうかに関係なく、特定の順序で要素をレイアウトしたい理由があります。コンパイラーを再配置したくないそれらの要素;

  2. 些細な量の既存のコードを破壊する可能性があります-構造体のアドレスが最初のメンバーのアドレスと同じであることに依存する多くのレガシーコードがあります(多くの古典的なMacOSを見ましたその仮定を行ったコード);

C99 Rationale は、2番目のポイントに直接対処し(「既存のコードは重要ですが、既存の実装は重要ではありません」)、間接的に1番目に対処します(「プログラマを信頼します」)。

10
John Bode

ポインター操作のセマンティクスを変更して、構造体メンバーの順序を変更します。コンパクトなメモリ表現を気にする場合、ターゲットアーキテクチャを理解し、それに応じて構造を整理するのはプログラマとしてのあなたの責任です。

9
vicatcu

C構造体との間でバイナリデータの読み取り/書き込みを行っている場合、structメンバーの並べ替えは大惨事になります。たとえば、実際にバッファから構造を取り込む実際的な方法はありません。

6
larsks

構造は、非常に低いレベルの物理ハードウェアを表すために使用されます。そのため、コンパイラは、そのレベルに合わせて物事を一巡することはできません。

ただし、プログラムの内部でのみ使用される純粋なメモリベースの構造体をコンパイラが再配置できる#pragmaを使用することは不合理ではありません。しかし、私はそのような獣を知りません(しかし、それはスクワットを意味しません-私はC/C++とは触れていません)

5
Peter M

構造体などの変数宣言は、変数の「パブリック」表現になるように設計されていることに注意してください。コンパイラーだけでなく、そのデータ型を表すものとして他のコンパイラーでも使用できます。おそらく.hファイルになります。したがって、コンパイラーが構造体内のメンバーの編成方法を自由にしようとする場合、すべてのコンパイラーは同じ規則に従う必要があります。そうしないと、前述のように、ポインター演算が異なるコンパイラ間で混同されます。

4
Kluge

structの最初の要素を並べ替える必要があるため、ケースは非常に具体的です。 structで最初に定義される要素は常にオフセット0にある必要があるため、これは不可能です。これが許可されると、多くの(偽の)コードが壊れます。

より一般的には、同じ大きなオブジェクト内にあるサブオブジェクトのポインターは、常にポインター比較を許可する必要があります。順序を逆にすると、この機能を使用する一部のコードが破損することを想像できます。そして、その比較のために、定義の時点でコンパイラの知識は役に立たないでしょう:サブオブジェクトへのポインタは、それがどのより大きなオブジェクトに属しているかを「マーク」しません。そのように別の関数に渡されると、可能なコンテキストのすべての情報が失われます。

2
Jens Gustedt

ここに私がこれまで見なかった理由があります-標準の再配置規則がなければ、ソースファイル間の互換性が壊れます。

構造体がヘッダーファイルで定義され、2つのファイルで使用されるとします。
両方のファイルは別々にコンパイルされ、後でリンクされます。コンパイルは、異なる時間(1つだけに触れたため、再コンパイルする必要があります)、異なるコンピューター(ファイルがネットワークドライブ上にある場合)、または異なるコンパイラーバージョンで行われます。
一度にコンパイラが並べ替えを決定し、別の順序では並べ替えを決定しない場合、2つのファイルはフィールドの場所について一致しません。

例として、statシステムコールとstruct stat
たとえば、Linuxをインストールすると、誰かがいつかコンパイルしたstatを含むlibCを取得します。
次に、最適化フラグを使用してコンパイラーでアプリケーションをコンパイルし、両方が構造体のレイアウトに同意することを期待します。

2
ugoren

ヘッダーa.hがあり、

struct s1 {
    char a;
    int b;
    char c;
    char d;
    char e;
}

これは別のライブラリの一部であり(未知のコンパイラによってコンパイルされたバイナリのみがコンパイルされている)、この構造体を使用してこのライブラリと通信したい場合、

コンパイラが好きな方法でメンバーを並べ替えることが許可されている場合これは不可能ですクライアントコンパイラとしてわからない構造体をそのまま使用するか、最適化する(そして、bを前後に移動する)か、4バイト間隔で整列したすべてのメンバーで完全にパディングするか

これを解決するために、圧縮のための決定論的アルゴリズムを定義できますが、それはすべてのコンパイラがそれを実装することを必要とし、アルゴリズムは(効率の点で)良いものです。再順序付けよりもパディング規則に同意する方が簡単です

#pragmaを追加するのは簡単です。これは、特定の構造体のレイアウトを必要なときに正確に最適化することを禁止するため、問題はありません。

1
ratchet freak