なぜsizeof
演算子は、構造体のメンバーの合計サイズよりも大きいサイズを構造体に返すのですか?
12 に答える
これは、位置合わせの制約を満たすためにパディングが追加されているためです。データ構造の調整は、プログラムのパフォーマンスと正確性の両方に影響を与えます。
- アクセスの位置がずれていると、ハードエラーになる可能性があります(多くの場合
SIGBUS
)。 - アクセスの位置がずれていると、ソフトエラーになる可能性があります。
- 適度なパフォーマンスの低下のために、ハードウェアで修正されました。
- または、ソフトウェアでのエミュレーションによって修正され、パフォーマンスが大幅に低下します。
- さらに、アトミック性およびその他の並行性保証が破られ、微妙なエラーが発生する可能性があります。
x86プロセッサの一般的な設定を使用した例を次に示します(すべて32ビットモードと64ビットモードを使用)。
struct X
{
short s; /* 2 bytes */
/* 2 padding bytes */
int i; /* 4 bytes */
char c; /* 1 byte */
/* 3 padding bytes */
};
struct Y
{
int i; /* 4 bytes */
char c; /* 1 byte */
/* 1 padding byte */
short s; /* 2 bytes */
};
struct Z
{
int i; /* 4 bytes */
short s; /* 2 bytes */
char c; /* 1 byte */
/* 1 padding byte */
};
const int sizeX = sizeof(struct X); /* = 12 */
const int sizeY = sizeof(struct Y); /* = 8 */
const int sizeZ = sizeof(struct Z); /* = 8 */
構造体のサイズを最小化するには、メンバーを配置で並べ替えます(基本タイプの場合はサイズで並べ替えるだけで十分です)(Z
上記の例の構造体のように)。
重要な注意:C標準とC ++標準の両方で、構造アラインメントは実装定義であると規定されています。したがって、各コンパイラはデータを異なる方法で整列することを選択する可能性があり、その結果、異なる互換性のないデータレイアウトが発生します。このため、さまざまなコンパイラで使用されるライブラリを扱う場合は、コンパイラがデータをどのように整列させるかを理解することが重要です。#pragma
一部のコンパイラには、構造アラインメント設定を変更するためのコマンドライン設定や特別なステートメントがあります。
ここの C FAQ で説明されているように、パッキングとバイト アラインメント:
アライメント用です。多くのプロセッサは、2 バイトや 4 バイトの量 (int や long int など) が詰め込まれているとアクセスできません。
次の構造があるとします。
struct { char a[3]; short int b; long int c; char d[3]; };
さて、この構造を次のようにメモリにパックできるはずだと思うかもしれません。
+-------+-------+-------+-------+ | a | b | +-------+-------+-------+-------+ | b | c | +-------+-------+-------+-------+ | c | d | +-------+-------+-------+-------+
ただし、コンパイラが次のように配置すると、プロセッサにとってははるかに簡単になります。
+-------+-------+-------+ | a | +-------+-------+-------+ | b | +-------+-------+-------+-------+ | c | +-------+-------+-------+-------+ | d | +-------+-------+-------+
パックされたバージョンでは、b フィールドと c フィールドがどのように折り返されているかを確認するのが少し難しいことに注意してください。簡単に言えば、プロセッサにとっても難しいことです。したがって、ほとんどのコンパイラは、次のように (追加の不可視フィールドがあるかのように) 構造体をパディングします。
+-------+-------+-------+-------+ | a | pad1 | +-------+-------+-------+-------+ | b | pad2 | +-------+-------+-------+-------+ | c | +-------+-------+-------+-------+ | d | pad3 | +-------+-------+-------+-------+
たとえば、構造をGCCで特定のサイズにしたい場合は、を使用します__attribute__((packed))
。
Windowsでは、cl.execompierを/Zpオプションとともに使用するときに、配置を1バイトに設定できます。
通常、プラットフォームやコンパイラによっては、CPUが4(または8)の倍数のデータにアクセスする方が簡単です。
つまり、基本的には調整の問題です。
あなたはそれを変える正当な理由を持っている必要があります。
これは、プラットフォーム上で構造が偶数のバイト (またはワード) になるように、バイト アラインメントとパディングが原因である可能性があります。たとえば、Linux の C では、次の 3 つの構造体:
#include "stdio.h"
struct oneInt {
int x;
};
struct twoInts {
int x;
int y;
};
struct someBits {
int x:2;
int y:6;
};
int main (int argc, char** argv) {
printf("oneInt=%zu\n",sizeof(struct oneInt));
printf("twoInts=%zu\n",sizeof(struct twoInts));
printf("someBits=%zu\n",sizeof(struct someBits));
return 0;
}
サイズ (バイト単位) がそれぞれ 4 バイト (32 ビット)、8 バイト (2x 32 ビット)、1 バイト (2+6 ビット) のメンバーを持ちます。上記のプログラム (gcc を使用する Linux 上) は、サイズを 4、8、および 4 として出力します。最後の構造体は、1 つの単語 (32 ビット プラットフォームでは 4 x 8 ビット バイト) になるようにパディングされます。
oneInt=4
twoInts=8
someBits=4
以下も参照してください。
Microsoft Visual C の場合:
http://msdn.microsoft.com/en-us/library/2e70t5y1%28v=vs.80%29.aspx
および GCC は、Microsoft のコンパイラとの互換性を主張しています。
https://gcc.gnu.org/onlinedocs/gcc-4.6.4/gcc/Structure_002dPacking-Pragmas.html
以前の回答に加えて、パッケージに関係なく、 C++ にはメンバー順保証がないことに注意してください。コンパイラは、仮想テーブル ポインターと基本構造体のメンバーを構造体に追加する場合があります (実際に追加します)。仮想テーブルの存在でさえ、標準では保証されていないため (仮想メカニズムの実装は指定されていません)、そのような保証はまったく不可能であると結論付けることができます。
C ではメンバーの順序が保証されていると確信していますが、クロスプラットフォームまたはクロスコンパイラプログラムを作成するときは、それを当てにしません。
パッキングと呼ばれるもののために、構造体のサイズはその部分の合計よりも大きくなります。特定のプロセッサには、動作する優先データ サイズがあります。32 ビット (4 バイト) の場合、最新のプロセッサの推奨サイズ。データがこの種の境界にあるときにメモリにアクセスすることは、そのサイズの境界にまたがるものよりも効率的です。
例えば。単純な構造を考えてみましょう:
struct myStruct
{
int a;
char b;
int c;
} data;
マシンが 32 ビット マシンで、データが 32 ビット境界で整列されている場合、すぐに問題が発生します (構造体の整列がない場合)。この例では、構造体データがアドレス 1024 (0x400 - 最下位 2 ビットがゼロであるため、データは 32 ビット境界に整列されていることに注意してください) から始まると仮定します。data.a へのアクセスは、境界 (0x400) で開始されるため、正常に機能します。data.b へのアクセスも正常に機能します。これは、別の 32 ビット境界であるアドレス 0x404 にあるためです。しかし、アラインされていない構造では、data.c がアドレス 0x405 に配置されます。data.c の 4 バイトは、0x405、0x406、0x407、0x408 にあります。32 ビット マシンでは、システムは 1 メモリ サイクル中に data.c を読み取りますが、4 バイトのうち 3 バイトしか取得しません (4 番目のバイトは次の境界にあります)。したがって、システムは 4 番目のバイトを取得するために 2 回目のメモリ アクセスを行う必要があります。
ここで、data.c をアドレス 0x405 に配置する代わりに、コンパイラが構造体に 3 バイトをパディングし、data.c をアドレス 0x408 に配置した場合、システムはデータを読み取るために 1 サイクルしか必要とせず、そのデータ要素へのアクセス時間を短縮します。 50% まで。パディングは、処理効率のためにメモリ効率を交換します。コンピュータが膨大な量のメモリ (数ギガバイト) を持つことができることを考えると、コンパイラはスワップ (サイズに対する速度) が合理的なものであると感じます。
残念ながら、この問題は、ネットワークを介して構造を送信しようとしたり、バイナリ データをバイナリ ファイルに書き込もうとしたりすると致命的になります。構造体またはクラスの要素間に挿入されたパディングは、ファイルまたはネットワークに送信されるデータを混乱させる可能性があります。移植可能なコード (いくつかの異なるコンパイラーに使用されるコード) を作成するには、構造体の各要素に個別にアクセスして、適切な「パッキング」を行う必要があります。
一方、コンパイラが異なれば、データ構造のパッキングを管理する機能も異なります。たとえば、Visual C/C++ では、コンパイラは #pragma pack コマンドをサポートしています。これにより、データのパッキングと配置を調整できます。
例えば:
#pragma pack 1
struct MyStruct
{
int a;
char b;
int c;
short d;
} myData;
I = sizeof(myData);
これで、I の長さは 11 になるはずです。プラグマがないと、コンパイラのデフォルトのパッキングに応じて、I は 11 から 14 まで (システムによっては 32 まで) になる可能性があります。
構造体のアラインメントを暗黙的または明示的に設定している場合は、これを行うことができます。4 でアラインされた構造体は、そのメンバーのサイズが 4 バイトの倍数ではない場合でも、常に 4 バイトの倍数になります。
また、ライブラリは x86 で 32 ビットの int を使用してコンパイルされる場合があり、そのコンポーネントを 64 ビット プロセスで比較すると、これを手動で行った場合に異なる結果が得られる場合があります。
C 言語では、メモリ内の構造要素の場所についてコンパイラにある程度の自由が与えられます。
- 2 つのコンポーネントの間、および最後のコンポーネントの後にメモリ ホールが発生する可能性があります。これは、ターゲット コンピューター上の特定の種類のオブジェクトがアドレス指定の境界によって制限される可能性があるためです。
- sizeof 演算子の結果に含まれる「メモリ ホール」のサイズ。sizeof には、C/C++ で利用可能な柔軟な配列のサイズのみが含まれていません。
- 言語の一部の実装では、プラグマとコンパイラ オプションを使用して構造体のメモリ レイアウトを制御できます。
C 言語は、構造内の要素レイアウトのプログラマーにある程度の保証を提供します。
- コンポーネントのシーケンスを割り当てるために必要なコンパイラ メモリアドレスを増やす
- 最初のコンポーネントのアドレスが構造体の開始アドレスと一致する
- 名前のないビット フィールドは、隣接する要素の必要なアドレス アラインメントに合わせて構造体に含めることができます。
要素の配置に関連する問題:
- さまざまなコンピューターがさまざまな方法でオブジェクトのエッジを並べます
- ビットフィールドの幅に関するさまざまな制限
- バイトを 1 ワードに格納する方法はコンピューターによって異なります (Intel 80x86 および Motorola 68000)。
アライメントの仕組み:
- 構造体が占める体積は、そのような構造体の配列の整列された単一要素のサイズとして計算されます。構造体は、次に続く構造体の最初の要素がアラインメントの要件に違反しないように終了する必要があります。
ps より詳細な情報については、「Samuel P.Harbison、Guy L.Steele CA リファレンス、(5.6.2 - 5.6.7)」を参照してください。
速度とキャッシュを考慮して、オペランドは本来のサイズに合わせてアドレスから読み取る必要があるという考え方です。これを実現するために、コンパイラは構造体メンバーをパディングして、次のメンバーまたは次の構造体が整列されるようにします。
struct pixel {
unsigned char red; // 0
unsigned char green; // 1
unsigned int alpha; // 4 (gotta skip to an aligned offset)
unsigned char blue; // 8 (then skip 9 10 11)
};
// next offset: 12
x86 アーキテクチャは、位置合わせされていないアドレスを常にフェッチすることができました。ただし、速度が遅く、ミスアライメントが 2 つの異なるキャッシュ ラインに重なる場合、アライメントされたアクセスでは 1 つしか削除されない場合でも、2 つのキャッシュ ラインが削除されます。
いくつかのアーキテクチャは、実際にはミスアライメントされた読み取りと書き込み、および ARM アーキテクチャの初期バージョン (今日のすべてのモバイル CPU に進化したもの) をトラップする必要があります。(下位ビットは無視されました。)
最後に、キャッシュラインは任意に大きくすることができ、コンパイラはそれらを推測したり、スペースと速度のトレードオフを試みたりしないことに注意してください。代わりに、アラインメントの決定は ABI の一部であり、最終的にキャッシュ ラインを均等に満たす最小のアラインメントを表します。
TL;DR:アラインメントは重要です。
他の回答に加えて、構造体には仮想関数を含めることができます (ただし、通常はそうではありません)。その場合、構造体のサイズには vtbl のスペースも含まれます。