突然ですが、C# で sizeof((byte, int))
の値はいくつでしょう?(64bit 環境)
byte
が1バイト、int
が4バイト、つまり (byte, int)
は5バイト……ではないです。
答えは8バイト。アライメント上に乗るようにパディングがあるからですね。
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|
byte | - | - | - | int (0) | int (1) | int (2) | int (3) |
では、sizeof((byte, int, short))
の値はいくつでしょう?(64bit 環境)
byte
, int
, short
がそれぞれ1, 4, 2 バイトで、アライメント上に乗せると4, 4, 4 で12バイト?
いいえ、答えは8バイトです。レイアウトは以下のようになります。
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|
byte | - | short (0) | short(1) | int (0) | int (1) | int (2) | int (3) |
はい。お察しの通り、ValueTuple はフィールドレイアウトが順番通りではないです。
(byte, int, short)
の場合、byte
, int
, short
の順番にアライメントに乗せるよりも、順番を入れ替えて byte
, short
, int
の順にそろえたほうが構造体のサイズを小さくできるためこのようになります。
なぜ順番が入れ替わるのでしょう?
たとえば (byte, int)
型の場合、(byte, int)
は糖衣構文で実際の型は ValueTuple<byte, int>
という型になります。
これは以下のように定義されています。
[StructLayout(LayoutKind.Auto)] public struct ValueTuple<T1, T2> { public T1 Item1; public T2 Item2; }
StructLayout(LayoutKind.Auto)
が指定されているので、コンパイラが都合よく自由にメモリレイアウトを決めるということです。
これによって効率よく配置されてメモリサイズを削減するようになる利点があります。
しかし、レイアウトはコンパイラが決めるためネイティブとの相互運用には使えなくなってしまうのです。 実際にはアライメントを考慮すればコンパイラが決めるレイアウトはわかるのですが、仕様上これは保証されません。
ネイティブライブラリとの相互運用で、構造体の型定義をサボって楽するための匿名型として ValueTuple は使えないんですね。
おつらい。