ねののお庭。

かりかりもふもふ。

【C#】struct のアライメントについて。

突然の C# クイズ!S01~S10 までの struct はそれぞれ何 byte でしょうか?

struct S01
{
    public byte Value1;
    public byte Value2;
    public byte Value3;
}

struct S02
{
    public short Value1;
    public byte Value2;
}

struct S03
{
    public byte Value1;
    public byte Value2;
    public byte Value3;
    public byte Value4;
    public byte Value5;
}

struct S04
{
    public short Value1;
    public short Value2;
    public byte Value3;
}

struct S05
{
    public int Value1;
    public byte Value2;
}

struct S06
{
    public int Value1;
    public byte Value2;
    public byte Value3;
}

struct S07
{
    public byte Value1;
    public int Value2;
    public byte Value3;
}

struct S08
{
    public short Value1;
    public short Value2;
    public short Value3;
    public short Value4;
    public byte Value5;
}

struct S09
{
    public int Value1;
    public int Value2;
    public byte Value3;
}

struct S10
{
    public long Value1;
    public byte Value2;
}

答えは以下の通り。

// unsafe context
Console.WriteLine(sizeof(S01)); // 3
Console.WriteLine(sizeof(S02)); // 4
Console.WriteLine(sizeof(S03)); // 5
Console.WriteLine(sizeof(S04)); // 6
Console.WriteLine(sizeof(S05)); // 8
Console.WriteLine(sizeof(S06)); // 8
Console.WriteLine(sizeof(S07)); // 12
Console.WriteLine(sizeof(S08)); // 10
Console.WriteLine(sizeof(S09)); // 12
Console.WriteLine(sizeof(S10)); // 16

一問でも間違えたら、是非この記事を読んでください:)

struct のアライメント

さて、C# クイズの答えの解説をしていきましょう。それぞれの struct の定義内容と sizeof の結果をまとめると以下のような形になっています。

  • S01 / S02 で定義されているフィールドの合計サイズは両方とも 3 byte ですが、それぞれのサイズは 3 / 4 byte です。
  • S03 / S04 / S05 で定義されているフィールドの合計サイズはどれも 5 byte ですが、それぞれのサイズは 5 / 6 / 8 byte です。
  • S06 / S07 で定義されているフィールドの合計サイズは両方とも 6 byte で、フィールドの定義順が異なるだけですが、それぞれのサイズは 8 / 12 byte です。
  • S08 / S09 / S10 で定義されているフィールドの合計サイズはどれも 9 byte ですが、それぞれのサイズは 10 / 12 / 16 byte です。

なぜこのような事になっているか?というのが気になるところでしょう。 これは struct のアライメントルールを把握すると理解できます。 ドキュメント上には、struct のアライメントは以下のようなルールで決定されていると記述されています。

  • 型自体は、以下の2つのどちらか小さい方のサイズでアライメントされる
    • 最もサイズの大きいフィールドのサイズ
    • StructLayoutAttribute.Pack で設定されたサイズ
      • Pack が既定値 (0) の場合は default pack size が用いられる
        • 補足1: ただし、default pack size の値についてはドキュメント上記載がなく、実装依存の模様
        • 補足2: Pack を明示的に指定しない限り「型自体は、最もサイズの大きいフィールドのサイズでアライメントされる」というのが殆どの場合に適用されるルールと認識しておけば概ね間違いはない。ただしあくまで殆どの場合であり、Vector128<T> 等の例外はある (後述)
  • 各フィールドは、以下の2つのどちらか小さい方のサイズでアライメントされる
    • フィールド自身のサイズ
    • 型のアライメントサイズ
      • 補足3: StructLayoutAttribute.Pack を明示的に設定している場合にのみ、フィールド自身のサイズより型のアライメントサイズが小さくなる場合がある
  • アライメントの要件を満たすために、フィールドとフィールドの間にパディングが入る事がある

具体例として、S08~S10 を見返してみましょう。

// 10 byte
struct S08
{
    public short Value1;
    public short Value2;
    public short Value3;
    public short Value4;
    public byte Value5;
}

// 12 byte
struct S09
{
    public int Value1;
    public int Value2;
    public byte Value3;
}

// 16 byte
struct S10
{
    public long Value1;
    public byte Value2;
}

S08 では short, S09 では int, S10 では long がそれぞれ最も大きいサイズのフィールドとなっています。 ですので、S08 は 2 byte, S09 は 4 byte, S10 は 8 byte でアライメントされる事になり、結果的に S08 / S09 / S10 のサイズは 10 / 12 / 16 byte になるわけです。 これが一つめのルールである「型自体は、最もサイズの大きいフィールドのサイズでアライメントされる」という事です。

次に struct 内のフィールドのアライメントについてみていきます。 以下のような struct を定義したとします。

struct S11
{
    public byte Value1;
    public short Value2;
    public long Value3;
}

この時、この struct の中身はこのような形になっています。

注目するべきは Value2 と Value3 のアライメントです。 Value2 は 2 byte (short) のフィールドのため、2 byte アライメントされます。このため、Value1 の直後ではなく、1 byte のパディングが挟まっています。Value3 は 8 byte (long) のフィールドなので 8 byte アライメントされます。このため Value3 は Value2 の直後ではなく、4 byte のパディングの後に配置されています。これが2つめのルールである「各フィールドはフィールド自身のサイズでアライメントされる」と3つめのルールである「アライメントの要件を満たすために、フィールドとフィールドの間にパディングが入る事がある」という事です。

ユーザ定義の struct のアライメント

再び C# クイズ! S07 は 12 byte でしたが、それでは以下の S12 は何 byte でしょうか?

// 12 byte
struct S07
{
    public byte Value1; // 1 byte
    // padding (3 byte)
    public int Value2; // 4 byte
    public byte Value3; // 1 byte
    // padding (3 byte)
}

// S07 の int (4 byte, primitive type) の代わりに
// S01 (3 byte, user defined type) を利用した
// S12 は何 byte でしょう?
struct S12
{
    public byte Value1;
    public S01 Value2;
    public byte Value3;
}

struct S01
{
    public byte Value1;
    public byte Value2;
    public byte Value3;
}

要するに、ユーザ定義の struct である S01 が、S12 のアライメントにどう効いてくるか?という問題です。 型のアライメントルールには「型自体は、最もサイズの大きいフィールドのサイズでアライメントされる」というルールが存在しますが、S01 を一つの塊として見るか見ないかで話が変わってきます。 ということで、考えられるパターンは以下の2パターンでしょう。正解はどちらでしょうか?

  • S01 が 3 byte なので、S12 は 3 byte アライメント
    • Value1 (1 byte) + padding (2 byte) + Value2 (3 byte) + Value3 (1 byte) + padding (2 byte)
  • S01 は 3 byte だが 1 byte アライメントなので、S12 も 1 byte アライメント
    • Value1 (1 byte) + Value2 (3 byte) + Value3 (1 byte)

正解は...後者です!

ドキュメント上明示されているのは「型自体は、最もサイズの大きいフィールドのサイズでアライメントされる」というルールですが、挙動見ている感じ 「型自体は、最もアライメントサイズの大きいフィールドのアライメントサイズでアライメントされる」 と認識した方が分かりがいい気がします。 要するに以下のような整理になります。

  • S12 の場合
    • byte
      • 1 byte / 1 byte アライメント
    • S01
      • 3 byte / 1 byte アライメント
    • 結果
      • 5 byte / 1 byte アライメント

もうちょっと具体例をみてみましょう。

struct S05
{
    public int Value1;
    public byte Value2;
}

struct S13
{
    public byte Value1;
    public S05 Value2;
}

struct S14
{
    public byte Value1;
    public S05 Value2;
    public byte Value3;
}

struct S15
{
    public byte Value1;
    public S05 Value2;
    public long Value3;
}

これは以下のようなメモリレイアウトになります。

  • S05 の場合
    • byte
      • 1 byte / 1 byte アライメント
    • int
      • 4 byte / 4 byte アライメント
    • 結果
      • 8 byte / 4 byte アライメント
      • 末尾に 3 byte の padding
  • S13 の場合
    • byte
      • 1 byte / 1 byte アライメント
    • S05
      • 8 byte / 4 byte アライメント
    • 結果
      • 12 byte / 4 byte アライメント
      • Value1 と Value2 の間に 3 byte の padding
  • S14 の場合
    • byte
      • 1 byte / 1 byte アライメント
    • S05
      • 8 byte / 4 byte アライメント
    • 結果
      • 16 byte / 4 byte アライメント
      • 当然ながら、Value2 の後ろの 3 byte は S05 という型に含まれる padding なので、Value3 が 1 byte アライメントだからといって、そこに踏み込んで詰めたりはできない
      • Value3 の後ろに 3 byte の padding
  • S15
    • byte
      • 1 byte / 1 byte アライメント
    • S05
      • 8 byte / 4 byte アライメント
    • long
      • 8 byte / 8 byte アライメント
    • 結果
      • 24 byte / 8 byte アライメント
      • Value2 の後ろに 4 byte の padding
        • Value2 と Value3 の間には 7 byte の padding があるが、そのうち 3 byte の padding は S05 がそもそも抱えているものなので、Value2 と Value3 の間は実質的に 4 byte の padding といえる
        • これは Value3 が long なので 8 byte アライメントにするため

ということで、結局のところアライメントは大概の場合において、primitive type のサイズに縛られている事になります。 なので必然的に 1, 2, 4, 8 byte アライメントの何れかになります (decimal 16 byte だが primitive type ではない)。StructLayoutAttribute.Pack でアライメントのサイズは設定可能ですが、設定可能な値は 1, 2, 4, 8, 16, 32, 64, 128 byte アライメントに制限されています。

ちなみに、primitive type ではないがアライメントに影響するものとして、SIMD 等を活用する際に用いる Vector128<T>, Vector256<T>, Vector512<T> が存在します。Vector128<T>, Vector256<T>, Vector512<T> に適用されている [StructLayout] には特別な設定はなされていないのですが、[Intrinsic] がついているため runtime がアライメントについて特別扱いしている気がします。

struct S16
{
    public byte Value1;
    public Vector256<byte> Value2;
    public byte Value3;
}

struct S17
{
    public byte Value1;
    public S18 Value2;
    public byte Value3;
}

// Vector256 同様の 32 byte の struct
// Vector256<T> には以下のように [StructLayout(LayoutKind.Sequential, Size = 32)] が適用されている。
// S18 に対して同様の [StructLayout] を適用しても、同じ結果にはならない。
[StructLayout(LayoutKind.Sequential, Size = 32)]
struct S18
{
    public long Value1;
    public long Value2;
    public long Value3;
    public long Value4;
}

図から分かる通り (S16 が途中で見切れてしまっていますが)、S16 は 32 byte アライメント、S17 は 8 byte アライメントになっています。

ヒープ上の struct のアライメント

.NET のヒープは 32 / 64 bit 環境でそれぞれ 4 / 8 byte にアライメントされます。 しかしながら、ここまで見てきた通り、struct は必ずしも 4 / 8 byte にアライメントされるわけではありません。 という事で、struct のアライメントとヒープのアライメントの関係を見ていきましょう。 なお、以降の説明や図は 64 bit 環境を前提とします。

class のフィールド

まずは素朴な class のレイアウトをみましょう。

class C01
{
    public byte Value1;
    public int Value2;
    public byte Value3;
}

struct の場合は既定ではフィールドは定義順に並びますが、class の場合はパフォーマンスを向上させるために順序が入れ替わる事が許容されているため、必ずしも定義順に並ぶわけではありません。 struct のようなレイアウトになってしまうと 32 byte (header 8 byte + type handle 8 byte + 1byte + 3 byte padding + 4 byte + 1 byte + 7 byte padding) 確保することになってしまいますが、図のように int, byte, byte という順序に入れ替えてしまえば 24 byte (header 8 byte + type handle 8 byte + 1byte + 1 byte + 2 byte padding + 4 byte) になります。 このように省メモリになることでパフォーマンスの向上が期待できます。 class は struct とは根本的に用途が異なるため、このような並び替えが既定で許容されています。 なお type handle についてはこちらで語っているので気になる方はご覧ください。

さて、ユーザ定義型はどうでしょうか?

class C02
{
    public S19 Value1;
    public byte Value2;
}

struct S19
{
    public byte Value1;
    public short Value2;
}

S19 は 4 byte / 2 byte アライメントの struct なので、C02 の Value1 と Value2 の間に 1 byte の padding が挟まり、S19 が 2 byte アライメントになっている事が見て取れます。 このあたりのアライメントは class も struct も同じですね。

配列

配列の要素と要素の間は基本的に隙間なくぴっちりと埋められます。 そして最後にヒープのアライメントに合わせてパディングされます。

// 5 byte / 1 byte アライメント
struct S03
{
    public byte Value1;
    public byte Value2;
    public byte Value3;
    public byte Value4;
    public byte Value5;
}

// 8 byte / 4 byte アライメント
struct S05
{
    public int Value1;
    public byte Value2;
}

S03, S05 の配列を確保するとこのようになります。 ただし、図中には表れていませんが、実際には 8 byte アライメントになるよう (64 bit 環境のため)、末尾には padding が含まれる事になります。

まとめ

既定では struct は以下のルールに従ってアライメントされます。

  • 型自体は、最もアライメントサイズの大きいフィールドのアライメントサイズでアライメントされる
  • 各フィールドは、フィールド自身のアライメントサイズでアライメントされる

既定から外れ [StructLayout] を用いていろいろ設定した場合は、上記のルールがもう少し複雑になり、以下のようなルールになります。

  • 型自体は、最もアライメントサイズの大きいフィールドのアライメントサイズか、StructLayoutAttribute.Pack で設定されたアライメントサイズの小さい方でアライメントされる
  • 各フィールドは、フィールド自身のアライメントサイズか、型のアライメントサイズの小さい方でアライメントされる

正直、メモリレイアウトを強く意識する場合においては、大概以下のように [StructLayout(LayoutKind.Explicit)] を指定して意図通りのレイアウトを構築してしまう事が殆どだとは思います。

struct S20
{
    public byte Value1;
    public long Value2;
    public byte Value3;
    public int Value4;
}

[StructLayout(LayoutKind.Explicit)]
struct S21
{
    [FieldOffset(0)]
    public byte Value1;
    [FieldOffset(1)]
    public long Value2;
    [FieldOffset(9)]
    public byte Value3;
    [FieldOffset(10)]
    public int Value4;
}

// LayoutKind.Explicit においても Pack = 1 つけないと、
// long の存在により 8 byte アライメントになるため padding が発生する。
// Pack を 1 に設定する事で 1 byte アライメントに強制できる。
[StructLayout(LayoutKind.Explicit, Pack = 1)]
struct S22
{
    [FieldOffset(0)]
    public byte Value1;
    [FieldOffset(1)]
    public long Value2;
    [FieldOffset(9)]
    public byte Value3;
    [FieldOffset(10)]
    public int Value4;
}

// せっかくなので比較用に LayoutKind.Auto も
[StructLayout(LayoutKind.Auto)]
struct S23
{
    public byte Value1;
    public long Value2;
    public byte Value3;
    public int Value4;
}

とはいえ、既定でどのようにアライメントされ、どのようなメモリレイアウトになるかは知っておいた方が何かと幸せになれるでしょう。 [StructLayout(LayoutKind.Explicit)] でレイアウトを明示するまではしなくても、雑な順序でフィールドを定義するよりは適切な順序で定義する事で struct のサイズがスリムになる可能性があります故。

おまけ

登場した型をまとめておきます。

struct S01
{
    public byte Value1;
    public byte Value2;
    public byte Value3;
}

struct S02
{
    public short Value1;
    public byte Value2;
}

struct S03
{
    public byte Value1;
    public byte Value2;
    public byte Value3;
    public byte Value4;
    public byte Value5;
}

struct S04
{
    public short Value1;
    public short Value2;
    public byte Value3;
}

struct S05
{
    public int Value1;
    public byte Value2;
}

struct S06
{
    public int Value1;
    public byte Value2;
    public byte Value3;
}

struct S07
{
    public byte Value1;
    public int Value2;
    public byte Value3;
}

struct S08
{
    public short Value1;
    public short Value2;
    public short Value3;
    public short Value4;
    public byte Value5;
}

struct S09
{
    public int Value1;
    public int Value2;
    public byte Value3;
}

struct S10
{
    public long Value1;
    public byte Value2;
}

struct S11
{
    public byte Value1;
    public short Value2;
    public long Value3;
}

struct S12
{
    public byte Value1;
    public S01 Value2;
    public byte Value3;
}

struct S13
{
    public byte Value1;
    public S05 Value2;
}

struct S14
{
    public byte Value1;
    public S05 Value2;
    public byte Value3;
}

struct S15
{
    public byte Value1;
    public S05 Value2;
    public long Value3;
}

struct S16
{
    public byte Value1;
    public Vector256<byte> Value2;
    public byte Value3;
}

struct S17
{
    public byte Value1;
    public S18 Value2;
    public byte Value3;
}

[StructLayout(LayoutKind.Sequential, Size = 32)]
struct S18
{
    public long Value1;
    public long Value2;
    public long Value3;
    public long Value4;
}

struct S19
{
    public byte Value1;
    public short Value2;
}

struct S20
{
    public byte Value1;
    public long Value2;
    public byte Value3;
    public int Value4;
}

[StructLayout(LayoutKind.Explicit)]
struct S21
{
    [FieldOffset(0)]
    public byte Value1;
    [FieldOffset(1)]
    public long Value2;
    [FieldOffset(9)]
    public byte Value3;
    [FieldOffset(10)]
    public int Value4;
}

[StructLayout(LayoutKind.Explicit, Pack = 1)]
struct S22
{
    [FieldOffset(0)]
    public byte Value1;
    [FieldOffset(1)]
    public long Value2;
    [FieldOffset(9)]
    public byte Value3;
    [FieldOffset(10)]
    public int Value4;
}

[StructLayout(LayoutKind.Auto)]
struct S23
{
    public byte Value1;
    public long Value2;
    public byte Value3;
    public int Value4;
}

class C01
{
    public byte Value1;
    public int Value2;
    public byte Value3;
}

class C02
{
    public S19 Value1;
    public byte Value2;
}

References