객체의 메모리 레이아웃에 대하여

[제목] 객체의 메모리 레이아웃에 대하여

클래스 혹은 구조체 객체의 크기는 일반적으로 필드 크기의 합보다 크게 표시되며, 객체의 필드들은 개발자가 정의한 순서대로 메모리 상에 위치하지 않는다. 이 노트는 이러한 현상의 이유와 해결법을 알아본다.

다음과 같은 구조체의 크기를 구해보자.

struct MyStruct
{
    public int i;     // 4
    public double d;  // 8
    public byte b;    // 1
}

구조체의 크기는 각 필드 크기의 합이므로, 이론적으로 MyStruct 구조체의 크기는 4+8+1 = 13 이다. 크기를 구하기 위해 다음과 같이 Marshal.SizeOf()를 사용할 수 있다.

int size = Marshal.SizeOf(typeof(MyStruct));

하지만 실제 이 문장을 실행해 보면, size가 24임을 알 수 있다. 이는 필드들을 메모리 바운더리 상에 정렬하는 Field Alignment 규칙에 따른 것이다. 즉, 위의 구조체의 경우 8바이트 바운더리에 맞춘 것이다. 이러한 Alignment는 C# / .NET에서 자동으로 처리되는데, 경우에 따라 개발자가 이를 변경할 필요가 있을 때가 있다. 예를 들어, 네트워크 스트림이나 파일 스트림에 위의 구조체가 정확히 13바이트로 존재해야 할 경우가 있을 수 있고, 또는 Unmanaged Code와 함께 데이타를 사용해야 할 경우가 있을 수 있다. 이러한 경우 정확한 Object 크기를 셋팅하기 위해 Pack이라는 속성을 지정할 수 있다. Pack=1은 1바이트로 정렬된다는 의미로 이를 지정하면 위의 MyStruct는 정확히 13 바이트를 갖게 된다.

아래 예제는 13 바이트의 구조체를 생성한 후 이를 바이너리 파일로 저장한 후, 파일 크기가 13 바이트임을 체크하는 코드이다.

[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct MyStruct
{
    public int i;     // 4
    public double d;  // 8
    public byte b;    // 1
}

static void Main(string[] args)
{
    int size = Marshal.SizeOf(typeof(MyStruct));
    Console.WriteLine(size);

    var s = new MyStruct();
    s.i = 1;
    s.d = 2;
    s.b = 3;

    byte[] buff = new byte[size];
    IntPtr ptr = Marshal.AllocHGlobal(size);

    Marshal.StructureToPtr(s, ptr, true);
    Marshal.Copy(ptr, buff, 0, size);
    Marshal.FreeHGlobal(ptr);

    string filename = @"c:\temp\1.txt";
    using (var fs = new FileStream(filename, FileMode.Create))
    {
        using (var wr = new BinaryWriter(fs))
        {
            wr.Write(buff);
        }
    }

    byte[] bytes = File.ReadAllBytes(filename);
    Console.WriteLine(bytes.Length);
}


이렇게 크기의 문제가 해결되었어도 한가지 남는 문제가 있다. 즉, 각 필드의 순서가 서로 틀려서는 안된다는 것이다. CLR은 기본적으로 Optimiazation의 일환으로 클래스 필드의 순서를 자동으로 변경하는데 이것을 Auto Layout이라 부른다. Auto Layout은 Managed Memory 상에서 클래스 필드의 순서를 자동으로 재배치하는데, 이러한 타입의 레이아웃은 Managed 영역밖으로 데이타를 Export하지 못한다. 즉, 위와 같은 ( Marshal.* ) 마샬링 코드를 사용할 수 없다. 만약 마샬링을 사용하면 런타임 에러가 발생한다.

C#에서 클래스와 같은 Reference Type은 디폴트로 Auto Layout을 사용하고, Struct와 같은 Value Type은 디폴트로 Sequential Layout을 사용한다. Sequential Layout은 Managed Memory에서 마샬링을 사용해 Unmanaged Memory로 옮길 때 각 필드의 순서가 Unmanaged Memory에서 유지되는 레이아웃이다. 위의 예제에서 MyStruct구조체는 [StructLayout(LayoutKind.Sequential)]을 사용하고 있는데, 이는 Managed 메모리 영역에서는 순서가 어떨지 모르지만, Unmanaged Memory로 옮겨질 때는 반드시 필드 순서대로 데이타가 옮겨진다는 것을 의미한다.

그렇다면, Managed Memory에서도 필드 순서가 유지되도록 할 수 있을까? 이를 위해 마지막 레이아웃 타입인 Explicit Layout이 존재한다. 이 레이아웃을 사용하면 클래스이든 구조체이든 상관없이 Managed Memory 상에서 필드에 지정된 FieldOffset() 속성에 따라 메모리 위치가 설정된다. 또한, 이 객체가 Unmanaged Memory로 옮겨질 때도 동일한 필드 위치를 유지하게 된다.

[StructLayout(LayoutKind.Explicit, Pack = 1)]
class MyClass
{
    [FieldOffset(0)]
    public int i;
    [FieldOffset(4)]
    public double d;
    [FieldOffset(12)]
    public byte b;
}

위의 코드는 MyClass 라는 클래스에 Explicit 레이아웃을 사용하여 필드 i 가 처음에, 필드 d가 4번째 바이트 위치에, 필드 b가 12번째에 각각 위치하게 됨을 지정하고 있다. 이러한 필드 위치는 Managed Heap에 MyClass객체가 생성될 때와 이 객체가 다시 Unmanaged Heap에 옮겨질 때 모두 그대로 유지된다.



본 웹사이트는 광고를 포함하고 있습니다. 광고 클릭에서 발생하는 수익금은 모두 웹사이트 서버의 유지 및 관리, 그리고 기술 콘텐츠 향상을 위해 쓰여집니다.