Data structure alignment (Memory layout)
Peponi │ 11/21/2024 │ 8m
Data structure alignment (Memory layout)
Peponi
1. Introduction
구조체 또는 클래스는 필드, 메서드 등을 캡슐화하는 데 사용한다. 선언된 멤버의 수에 따라 크기가 달라지게 되는데, 그 크기는 sizeof, Marshal.SizeOf로 구할 수 있다.
구조체 또는 클래스를 사용하다 보면 정의한 크기보다 크거나 필드의 위치가 바뀌는 일이 발생하는데, 이를 해결하기 위해 어느 정도 메모리 레이아웃 규칙에 대해 알 필요가 있다.
2. 기본 레이아웃
다음 구조체의 크기는 C#의 형식 기본 크기에 따라 3byte가 되어야 한다. 하지만 실제로 크기를 조사해보면, 4byte가 나오게 된다.
public struct Test
{
public byte A; // 1 byte
public short B; // 2 byte
}
static void Main(string[] args)
{
Console.WriteLine(Marshal.SizeOf<Test>());
}
/* output:
4
*/
다음 구조체의 크기는 11byte로 예상되지만, 실제로는 16byte가 나오게 된다.
public struct Test
{
public byte A; // 1 byte
public short B; // 2 byte
public double C; // 8 byte
}
static void Main(string[] args)
{
Console.WriteLine(Marshal.SizeOf<Test>());
}
/* output:
16
*/
상기 예제의 현상을 자세히 파악하기 위해 메모리 할당을 확인해보면 아래와 같다.
unsafe static void Main(string[] args)
{
Test t = new();
var addr = (byte*)&t;
Console.WriteLine(Marshal.SizeOf<Test>());
Console.WriteLine($"Offset : {(byte*)&t.A - addr}");
Console.WriteLine($"Offset : {(byte*)&t.B - addr}");
Console.WriteLine($"Offset : {(byte*)&t.C - addr}");
Console.WriteLine($"Size : {Marshal.SizeOf(t.A)}");
Console.WriteLine($"Size : {Marshal.SizeOf(t.B)}");
Console.WriteLine($"Size : {Marshal.SizeOf(t.C)}");
}
/* output:
16
Offset : 0
Offset : 2
Offset : 8
Size : 1
Size : 2
Size : 8
*/
상기 예제와 같은 현상이 발생하는 이유는 아래에서 확인한다.
3. 메모리 레이아웃 규칙
C#의 기본 메모리 레이아웃에는 아래 두 가지 규칙이 적용된다.
- 각 필드는 선언된 순서에 따라 메모리에 할당된다.
- 모든 필드 중 가장 큰 변수의 크기를 기준으로 필드들이 정렬된다.
- 예로, 가장 큰 변수의 크기가
short
라면 2byte,double
이라면 8byte 단위로 필드가 정렬된다.
- 예로, 가장 큰 변수의 크기가
위 규칙을 이용하여 중간에 비어 있는 메모리 공간이 최소화되도록 상기 예제를 정렬하면 아래와 같다.
public struct Test
{
public double A;
public short B;
public byte C;
}
unsafe static void Main(string[] args)
{
Test t = new();
var addr = (byte*)&t;
Console.WriteLine(Marshal.SizeOf<Test>());
Console.WriteLine($"Offset : {(byte*)&t.A - addr}");
Console.WriteLine($"Offset : {(byte*)&t.B - addr}");
Console.WriteLine($"Offset : {(byte*)&t.C - addr}");
Console.WriteLine($"Size : {Marshal.SizeOf(t.A)}");
Console.WriteLine($"Size : {Marshal.SizeOf(t.B)}");
Console.WriteLine($"Size : {Marshal.SizeOf(t.C)}");
}
/* output:
16
Offset : 0
Offset : 8
Offset : 10
Size : 8
Size : 2
Size : 1
*/
위의 규칙을 잘 이용하면, 구조체를 효율적으로 작성할 수 있다.
public struct Inefficient
{
public byte A;
public int B;
public byte C;
public short D;
}
static void Main(string[] args)
{
Console.WriteLine(Marshal.SizeOf<Inefficient>());
}
/* output:
12
*/
public struct Efficient
{
public byte A;
public byte C;
public short D;
public int B;
}
static void Main(string[] args)
{
Console.WriteLine(Marshal.SizeOf<Efficient>());
}
/* output:
8
*/
4. 메모리 레이아웃 변경
통신을 하거나 dll에 구조체를 넘기는 등의 작업을 할 때는 레이아웃이 비효율적이더라도 따라야 하고, 크기 역시 강제되어 있는 경우가 있다. .NET에서 자동으로 처리되는 메모리 구조가 이에 맞지 않는 경우가 발생할 수 있는데, 다음 어트리뷰트를 사용해 레이아웃을 변경할 수가 있다.
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct Foo
{
// Some members...
}
항목 | 세부 항목 | 내용 |
---|---|---|
Layoutkind | Sequential |
|
Explicit |
| |
Auto |
| |
Pack |
|
앞 부분의 정리되지 않은 Test
struct
에 StructLayout
어트리뷰트를 달아 레이아웃 정렬이 가능하며, 할당되는 크기를 줄일 수 있다.
[StructLayout(LayoutKind.Explicit, Pack = 1)]
public struct Test
{
[FieldOffset(10)]
public byte A;
[FieldOffset(8)]
public short B;
[FieldOffset(0)]
public double C;
}
unsafe static void Main(string[] args)
{
Test t = new();
var addr = (byte*)&t;
Console.WriteLine(Marshal.SizeOf<Test>());
Console.WriteLine($"Offset : {(byte*)&t.A - addr}");
Console.WriteLine($"Offset : {(byte*)&t.B - addr}");
Console.WriteLine($"Offset : {(byte*)&t.C - addr}");
Console.WriteLine($"Size : {Marshal.SizeOf(t.A)}");
Console.WriteLine($"Size : {Marshal.SizeOf(t.B)}");
Console.WriteLine($"Size : {Marshal.SizeOf(t.C)}");
}
/* output:
11
Offset : 10
Offset : 8
Offset : 0
Size : 1
Size : 2
Size : 8
*/
5. Win32 BOOL type
기본적으로 bool
형식은 마샬링이 일어날 시 형식이 달라지게 된다. Managed type
의 bool
은 크기가 1
byte지만 Unmanaged memory
로 마샬링되는 bool
은 Win32의 BOOL
type (INT
로 정의됨) 으로 변환되며 4
byte의 크기를 가지게 된다. 따라서 통신 등에 활용 시 주의가 필요한 경우가 발생할 수 있다.
public struct Test
{
public bool A;
}
static void Main(string[] args)
{
Console.WriteLine(Marshal.SizeOf<Test>());
}
/* output:
4
*/
6. 참조 자료
- StructLayoutAttribute Class
- StructLayoutAttribute.Pack Field
- CA1414: MarshalAs로 부울 P/Invoke 인수를 표시합니다.
- 객체의 메모리 레이아웃에 대하여
- Sizeof operator