web-dev-qa-db-ja.com

コンパイラによる構造の並べ替え

次のような構造体があるとします。

struct MyStruct
{
  uint8_t var0;
  uint32_t var1;
  uint8_t var2;
  uint8_t var3;
  uint8_t var4;
};

これは、おそらく(トンではなく)たくさんのスペースを無駄にすることになります。これは、uint32_t変数の必要な配置のためです。

実際には(実際にuint32_t変数を使用できるように構造を調整した後)、次のようになります。

struct MyStruct
{
  uint8_t var0;
  uint8_t unused[3];  //3 bytes of wasted space
  uint32_t var1;
  uint8_t var2;
  uint8_t var3;
  uint8_t var4;
};

より効率的な構造体は次のとおりです。

struct MyStruct
{
  uint8_t var0;
  uint8_t var2;
  uint8_t var3;
  uint8_t var4;
  uint32_t var1;
};

さて、問題は:

コンパイラが構造体を並べ替えることを(標準により)禁止されているのはなぜですか?

構造体を並べ替えた場合、足で自分を撃つことができる方法は見当たらない。

34
DarthRubik

コンパイラが構造体を並べ替えることを(標準により)禁止されているのはなぜですか?

基本的な理由は、Cとの互換性のためです。

Cは元々、高水準のアセンブリ言語であることを覚えておいてください。 Cでは、バイトを特定のstructとして再解釈してメモリ(ネットワークパケットなど)を表示するのが一般的です。

これにより、このプロパティに依存する複数の機能が生まれました。

  • Cは、structのアドレスとその最初のデータメンバーのアドレスが同じであることを保証しているため、C++でも同じです(virtualの継承/メソッドがない場合)。

  • Cは、2つのstructABがあり、両方がデータメンバーcharで始まり、その後にデータメンバーint(およびその後)が続く場合、それらをunionメンバーに入れてBメンバーを書き込み、charを読み取ることができることを保証しましたintとそのAメンバーを使用するため、C++でも次のようになります: Standard Layout

後者はextremely幅が広く、ほとんどのstruct(またはclass)のデータメンバーの並べ替えを完全に防ぎます。


標準では一部の並べ替えが許可されていることに注意してください。Cにはアクセス制御の概念がなかったため、C++では、異なるアクセス制御指定子を持つ2つのデータメンバーの相対的な順序は指定されていません。

私の知る限り、コンパイラはそれを利用しようとしません。しかし、理論的には可能でした。

C++以外では、Rustなどの言語では、コンパイラがフィールドを並べ替えることができ、メインのRustコンパイラ(rustc)は、デフォルトでそうします。過去の決定と下位互換性に対する強い要望があるため、C++はそうすることができません。

37
Matthieu M.

構造体が再注文された場合、あなたが自分の足を撃つことができる方法は見当たらない。

本当に?これが許可された場合、同じプロセス内であってもライブラリ/モジュール間の通信はデフォルトでばかげて危険になります。

「宇宙で」の議論

知っている構造体は、要求したとおりに定義されている必要があります。パディングが指定されていないのは残念です!幸い、必要なときにこれを制御できます。

さて、理論的には、新しい言語を作成して、同様にメンバーを並べ替えることができます属性が指定されていない限り。結局のところ、オブジェクトに対してメモリレベルのマジックを実行することは想定されていないため、C++のイディオムのみを使用する場合は、デフォルトで安全です。

しかし、それは私たちが住んでいる実際の現実ではありません。


「宇宙の外」の議論

あなたの言葉で言えば、「毎回同じリオーダーが使用された」なら、物事を安全にすることができます。言語は、メンバーがどのように注文されるかを明確に述べなければなりません。これは、標準で記述するのが複雑であり、理解するのも複雑であり、実装するのも複雑です。

順序がコードのとおりであることを保証し、これらの決定はプログラマーに任せる方がはるかに簡単です。これらのルールには、Originが古いCにあり、古いCはprogrammerに権限を与えます。

簡単なコード変更で構造体のパディングを効率化するのがどれほど簡単かは、すでに質問で示しました。これを行うために、言語レベルで複雑さを追加する必要はありません。

標準では、構造体がデータプロトコルやハードウェアレジスタのコレクションなどの特定のメモリレイアウトを表す場合があるため、割り当て順序が保証されています。たとえば、プログラマもコンパイラも、TPC/IPプロトコルのバイトの順序や、マイクロコントローラのハードウェアレジスタを自由に並べ替えることはできません。

順序が保証されなかった場合、structsは単なる抽象データコンテナ(C++ベクトルと同様)になりますが、内部に配置したデータが何らかの形で含まれていることを除いて、その多くを想定することはできません。どんな形の低レベルのプログラミングをするときでも、それらは実質的にもっと役に立たなくなるでしょう。

14
Lundin

構造体が別のコンパイラーまたは別の言語によって生成された他の低レベルコードによって読み取られた場合、コンパイラーはメンバーの順序を維持する必要があります。たとえば、オペレーティングシステムを作成していて、その一部をCで記述し、一部をアセンブリで記述することにしたとします。次の構造を定義できます。

struct keyboard_input
{
    uint8_t modifiers;
    uint32_t scancode;
}

これをアセンブリルーチンに渡します。ここで、構造のメモリレイアウトを手動で指定する必要があります。 4バイトアライメントのシステムで次のコードを記述できると期待できます。

; The memory location of the structure is located in ebx in this example
mov al, [ebx]
mov edx, [ebx+4]

ここで、コンパイラーが実装で定義された方法で構造体のメンバーの順序を変更するとします。これは、使用するコンパイラーとそれに渡すフラグに応じて、スキャンコードの最初のバイトになる可能性があることを意味しますalのメンバー、またはmodifiersメンバー。

もちろん、この問題は、アセンブリルーチンを使用した低レベルのインターフェイスに限定されるだけでなく、異なるコンパイラでビルドされたライブラリが相互に呼び出した場合にも発生します(たとえば、Windows APIを使用してmingwでプログラムを構築)。

このため、この言語では、構造のレイアウトについて考えることを強いられています。

7
Shadowwolf

要素を自動的に並べ替えてパッキングを向上させるだけでなく、特定のメモリレイアウトやバイナリシリアライゼーションを損なう可能性があるだけでなく、プログラマがプロパティの順序を注意深く選択して、頻繁に使用されるメンバーのキャッシュの局所性を損なう可能性があることに注意してください。まれにしかアクセスされません。

5
ZaldronGG

Dennis Ritchieによって設計された言語は、動作の観点からではなく、メモリレイアウトの観点から構造のセマンティクスを定義しました。構造体SにオフセットXにタイプTのメンバーMがある場合、MSの動作はSのアドレスを取得し、それにXバイトを追加し、それをTへのポインターとして解釈し、それによって識別されたストレージを次のように解釈することとして定義されました。左辺値。構造体メンバーを書き込むと、関連するストレージの内容が変更され、メンバーのストレージの内容を変更すると、メンバーの値が変更されます。コードは、構造体メンバーに関連付けられたストレージを操作するさまざまな方法を自由に使用でき、セマンティクスは、そのストレージでの操作に関して定義されます。

コードが構造に関連付けられたストレージを操作できる便利な方法の1つは、memcpy()を使用して1つの構造の任意の部分を別の構造の対応する部分にコピーするか、memset()を使用して構造の任意の部分をクリアすることでした。構造体メンバーは順番に配置されているため、単一のmemcpy()またはmemset()呼び出しを使用して、メンバーの範囲をコピーまたはクリアできます。

標準委員会によって定義された言語は、多くの場合、構造体メンバーへの変更が基礎となるストレージに影響を与える必要がある、またはストレージへの変更がメンバー値に影響を与えるという要件を排除し、Ritchieの言語であった場合よりも構造体レイアウトの有用性を低下させます。それにもかかわらず、memcpy()とmemset()を使用する機能は保持され、その機能を保持するには構造要素を連続的に保つ必要がありました。

4
supercat

あなたはC++も引用しているので、それが起こり得ない実際的な理由を説明します。

classstruct の間に違いはないので、以下を考慮してください:

class MyClass
{
    string s;
    anotherObject b;

    MyClass() : s{"hello"}, b{s} 
    {}

};

現在、C++では、非静的データメンバーを宣言された順序で初期化する必要があります。

—次に、非静的データメンバーは、クラス定義で宣言された順序で初期化されます

[base.class.init/13] 。そのため、コンパイラーはクラス定義内のフィールドの順序を変更できません。それ以外の場合(例として)他のメンバーの初期化に依存するメンバーは機能しなかったためです。

コンパイラは、メモリ内でそれらを並べ替えないことを厳密に要求されるわけではありません(私が言えることのために)—特に、上記の例を考えると、それを追跡することは非常に困難です。そして、パディングとは異なり、パフォーマンスの向上は疑わしい。

4
edmz