12

C++ で単純なクラス Storer を作成し、メモリ割り当てを操作しました。これには 6 つのフィールド変数が含まれており、それらはすべてコンストラクターで割り当てられます。

int x;
int y;
int z;
char c;
long l;
double d;

これらの変数がどのように格納されているかに興味があったので、次のコードを書きました。

Storer *s=new Storer(5,4,3,'a',5280,1.5465);
cout<<(long)s<<endl<<endl;
cout<<(long)&(s->x)<<endl;
cout<<(long)&(s->y)<<endl;
cout<<(long)&(s->z)<<endl;
cout<<(long)&(s->c)<<endl;
cout<<(long)&(s->l)<<endl;
cout<<(long)&(s->d)<<endl;

私は出力に非常に興味がありました:

33386512

33386512
33386516
33386520
33386524
33386528
33386536

文字 c が 4 バイトを占めるのはなぜですか? sizeof(char) はもちろん 1 を返します。では、なぜプログラムは必要以上のメモリを割り当てているのでしょうか? これは、次のコードで割り当てられているメモリが多すぎることが確認されています。

cout<<sizeof(s->c)<<endl;
cout<<sizeof(Storer)<<endl;
cout<<sizeof(int)+sizeof(int)+sizeof(int)+sizeof(char)+sizeof(long)+sizeof(double)<<endl;

これは次を印刷します:

1
32
29

実際、3 バイトが不必要に割り当てられていることを確認します。なぜこれが起こっているのか誰にも説明できますか?ありがとう。

4

5 に答える 5

24

データの配置とコンパイラのパディングはこんにちは!

CPU には型の概念がなく、32 ビット (または 64 ビット、または 128 ビット (SSE)、または 256 ビット (AVX) - 32 で単純にしましょう) レジスタで取得するものは適切である必要があります。正しく効率的に処理するために調整されます。char の後に int が続く単純なシナリオを想像してみてください。32 ビット アーキテクチャでは、char が 1 バイト、integer が 4 バイトです。

32 ビット レジスタは境界でブレークする必要があり、整数の 3 バイトのみを取り込み、「2 回目の実行」のために 4 番目のバイトを残します。そのようにデータを適切に処理できないため、コンパイラはすべてのものを効率的に処理するためにパディングを追加します。これは、問題のタイプに応じて、一定量のパディングを追加することを意味します。

なぜミスアライメントが問題になるのですか?

コンピューターは人間ではありません。2 つの目と脳だけでそれらを識別することはできません。物事をどのように行うかについて、非常に決定論的で慎重でなければなりません。最初に、与えられた情報の n バイトを含む 1 つのブロックをロードし、関係のない情報を取り除くようにシフトします。次に、別のブロックをシフトして、目の前の操作とは関係のない不要なバイトの束を取り除きます。そうして初めて、必要な操作を実行できます。通常は 2 つのオペランドがありますが、これは完全な 1 つのみです。すべての作業を行って初めて、実際に処理することができます。単純にデータを適切に整列できる場合、パフォーマンスのオーバーヘッドが大きすぎます (そして、たいていの場合、特別なことをしていない場合は、コンパイラが代わりにそれを行います)。

あなたはそれを視覚化できますか?

視覚的には、最初の緑のバイトは前述の char であり、2 番目のブロックの 3 つの緑のバイトと最初の赤のバイトは 4 バイトの int であり、4 バイトのアクセス境界で色分けされています (32 ビットについて話しています)。登録)。下部の「代わりの部分」は、int がレジスターに適切にヒットする理想的なセットアップを示しています (char は、画像のどこかで服従にパディングされます)。

ここに画像の説明を入力

SSE (128 ビット regs) や AVX (256 ビット regs) のような高度な命令セットの拡張を扱う場合に非常に便利なデータ アライメントの詳細を参照してください。ベクトル化は無効になりません (SSE の 16 バイト境界で整列、16*8 -> 128 ビット)。

ユーザー定義の位置合わせに関する追加の注意事項

phonetaggerは、ユーザーがプログラマーが指定する方法でデータを整列させるために、プリプロセッサーを介して強制的にコンパイラーに割り当てることができるプラグマ・ディレクティブがあることをコメントで有効に指摘しました。しかし、そのようなディレクティブ ( など#pragma pack(...)) は、自分が何をしていて何が最善かを知っているというコンパイラーへのステートメントです。環境に適応できない場合、さまざまなペナルティが発生する可能性があるため、必ず実行してください。最も明白なのは、データをパックする方法が異なる、自分で作成していない外部ライブラリを使用することです。

物事は衝突すると爆発するだけです。そのような場合には注意を促し、目の前の問題に本当に親密になることが最善です. よくわからない場合は、デフォルトのままにしてください。よくわからないが、アラインメントが王様である SSE のようなものを使用する必要がある場合 (そして、デフォルトでも単純でもない場合)、オンラインでさまざまなリソースを参照するか、ここで別の質問をしてください。

于 2012-06-29T20:10:01.973 に答える
5

理解を助けるために類推を行います。

長いパンがあり、それを同じ厚さのスライスにカットできる切断機があるとします。次に、これらのパンを、たとえば子供たちに配っています。すべての子供は自分のパンを手に取り、やりたいことを公正に行います(ヌテラをのせて食べるなど)。彼らはそれからさらに薄いスライスを作り、そのように使うことさえできます.

1 人の子供があなたのところに来て、みんなが得ているスライスではなく、代わりに薄いスライスを望んでいると言った場合、切断機は少なくとも最小限の量を切断するように最適化されているため、誰もが幸せになるため、問題が発生します。 . しかし、1 人の子供がより薄いスライスを要求すると、機械を再発明するか、2 つの切断モードを導入するなど、機械をさらに複雑にする必要があります。あなたはそれを望んでいません。最終的にはあきらめて、とにかく彼に大きなスライスを与えます。

これが起こる理由と同じです。類推に共感していただければ幸いです。

于 2012-06-29T20:08:56.807 に答える
3

データの整列は、char が 4 バイトを割り当てた理由です:データの整列

于 2012-06-29T20:03:09.577 に答える
2

charは 4 バイトを使用しません。通常どおり 1 バイトを使用します。印刷して確認できますsizeof(char)。残りの 3 バイトは、クラスの他のメンバーへのアクセスを最適化するためにコンパイラが挿入するパディングです。ハードウェアによっては、4 で割り切れるアドレスに配置されている場合、マルチバイト型 (たとえば 4 バイト整数) にアクセスする方がはるかに高速であることがよくあります。コンパイラは、int メンバーの前に最大 3 バイトのパディングを挿入して、アクセスを高速化するための適切なメモリ アドレスに揃えることができます。

クラス レイアウトを試してみたい場合は、 と呼ばれる便利な操作を使用できますoffsetof。メンバーの名前とクラスの名前の 2 つのパラメーターを取り、構造体のベース アドレスからメモリ内のメンバーの位置までのバイト数を返します。

cout << offsetof(Storer, x) << endl;
cout << offsetof(Storer, y) << endl;
cout << offsetof(Storer, z) << endl;
于 2012-06-29T20:02:45.077 に答える
1

構造体のメンバーは、特定の方法で配置されます。一般に、最もコンパクトな表現が必要な場合は、メンバーをサイズの降順にリストします。

http://en.wikipedia.org/wiki/Data_structure_alignment#Typical_alignment_of_C_structs_on_x86

于 2012-06-29T20:03:21.160 に答える