メモリアライメントとは?
なぜ同じ構造体なのにメモリサイズが違うのか?
本ブログ作成文は、講義を目指して製作されました。 また、作成された文章の情報も間違っている可能性がありますので、ご注意ください。
コード化してみると、構造体のサイズが私の考えとは異なる方法で捉えることがあります。この現象を正式名称で Memory Alignment(メモリアライメント)およびPadding(パディング)と呼ぶ。
CPUはメモリから変数を読み込むとき、1バイト単位で分割して読み取らない。 これはワード単位で(通常4 Byteまたは8 Byte)ムントンが読み込まれるため、変数がメモリにどのように配置されているかがパフォーマンスにとって非常に重要です。
構造体のメンバーがメモリにどのように配置されているかによって、構造体の全体のサイズが変わることがあります。 これはパディングという概念によるものです。パディングは、構造体のメンバーがCPUのワード境界に合わせて整列するのを助けるためにコンパイラが自動的に選び込む空きスペースです。 途中に空きスペースが入り、当然構造体の大きさも増えることになる。
コードで直接確認してみましょう。
#include <iostream>
#include <cstddef> //offsetof マクロの使用
//1. 非効率的なメモリ配置(合計24バイト)
struct BadOrder {
int A; //4バイト、次の変数は8バイトのソートが必要なので、4バイトのパディングが発生しました(合計8バイトを使用)。
long long B; //8 Byte、単独で8 Byteを使用。 (合計16バイト使用)
bool C; //1バイト、1バイト使用後7バイトパディングが発生しました(合計24バイト使用).
};
//2. コンパイラの最適化を考慮した効率的なメモリ配置(合計16バイト)
struct GoodOrder {
long long A; //8 Byte、単独で8 Byteを使用。 (合計8バイト使用)
int B; //4 Byte、残りの4 Byteでは、次の変数を使用できるため、パディングは発生しません。 (合計12バイト使用)
bool C; //1 Byte、1 Byte 使用後 3 Byte パディング発生。 (合計16バイト使用)
};
//3. パディングを強制的に取り除いた構造体(合計13バイト)
#pragma pack(push, 1)
struct PackedStruct {
int A; // 4 Byte
long long B; // 8 Byte
bool C; // 1 Byte
};
#pragma pack(pop)
ここで気になる点がある。 BadOrderの終わりには7 Byte、GoodOrderの最後には3 Byteのパディングがあえてなぜもっと付くのか? これは、構造体が配列などで束ねられる時を備えた整列要件のためである。 構造体の合計サイズは、内部メンバの中で最大のデータ型(上記ではlong longの8バイト)の倍数でなければなりません。 だからこそ、配列の次の要素に進むときも、同じように8 Byteのソートが保証されるからです。
3番は#pragma packを使ってパディングを強制的に取り除く方法だ。 これにより、メモリはちょうど13バイトになることができますが、致命的なパフォーマンス低下が発生する可能性があります。 CPUがワード単位で読み出してデータの始点がずれている場合、 CPUはメモリを2回読み込んで無理やり合う無駄な追加演算をしなければならない。特定のハードウェア環境では、プログラムが伸びてしまう可能性があります。
また、キャッシュラインの効率も無視できません。 現代CPUはメモリを通常64 Byte単位で丸ごと取り込むが、1番のように24 Byteなら64 Byteの中に2つしか入れないが、 2番のように16 Byteに最適化すると、4つがすっきりすべて入る。配列を反復ドアに回すと、処理速度に大きな違いが出ます。
ネットワーク通信規格やファイル入出力のように1バイトの誤差もなくメモリレイアウトが正確でなければならない特殊な場合でなければ、 一般的な状況では、パディングを許可することはパフォーマンスの面ではるかに利得です。 したがって、2番のように大きさの大きいメンバーから(または逆順)束ねて配置して不要なパディングを最小化する習慣を挙げるのが良い。