언어/C#

C#의 메모리 정렬 (StructLayout)

tsyang 2025. 4. 17. 01:50

 

2021.04.18 - [이론/일반] - 객체 메모리, Object Alignment

 

객체 메모리, Object Alignment

#include class AAA { private: int intA; int intB; double doubleA; }; class BBB { private: int intA; double doubleA; int intB; }; int main() { AAA a; BBB b; std::cout

tsyang.tistory.com

 

메모리 정렬 및 False Sharing에 대한 내용은 위 글 참고

 

 

 

StructLayout


 

StructLayout 어트리뷰트를 struct/Class에 달아서 레이아웃을 지정해줄 수 있다.

 

[StructLayout(LayoutKind.Sequential)]
public struct SubStruct
{
    public byte X;
    public byte Y;
}

 

StructLayout은 인자로 LayoutKind를 넘겨줘야 한다.

 

LayoutKind에는 3가지 종류가 있다.

 

Sequential

struct의 기본 값이다.

필드를 정의한 순서대로 메모리에 배치된다. 두번째 인자로 Pack을 넘겨줄 수 있는데 이게 정렬 단위가 된다.

[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct Pack1Struct
{
    public byte B1;     // offset 0
    public double D1;   // offset 1 (1의 배수에 위치)
    
    //크기 : 9Byte
}

[StructLayout(LayoutKind.Sequential, Pack = 4)]
struct Pack4Struct
{
    public byte B1;     // offset 0
    public double D1;   // offset 4 (4의 배수에 위치)
    
    //크기 : 12Byte
}

[StructLayout(LayoutKind.Sequential, Pack = 8)]
struct Pack8Struct
{
    public byte B1;     // offset 0
    public double D1;   // offset 8 (8바이트 정렬 허용 → 패딩 발생)
    
    //크기 : 16Byte
}

 

64비트 아키텍처라면 아마 Pack=8이 기본값.

 

 

중첩된 경우에 flatten을 하는데 약간 주의가 필요하다.

[StructLayout(LayoutKind.Sequential)]
struct Inner
{
    public int A;   // offset 0
    public byte B1;    // offset 5
    //padding 5~7
    
    // Size : 8Byte
}

[StructLayout(LayoutKind.Sequential)]
struct Outer
{
    public Inner I;   //offset 0
    public byte B2;   //offset 8
    //padding 9~11
    
    // SIZE : 12Byte
}

 

Inner은 8바이트이다. 따라서 Outer의 크기는 "가장 큰 필드의 배수가 되어야 한다"는 법칙에 따라 16Byte라고 생각할지 모르겠지만, 내부적으로 flatten을 해주기때문에 Inner의 가장 큰 필드인 int크기의 배수로 정렬된다.

 

그렇다고 완전 풀어해쳐저서 아래와 같이 되지는 않는다.

[StructLayout(LayoutKind.Sequential)]
struct Flattened
{
    public int A;   // offset : 0
    public byte B1;    // offset : 5
    public byte B2;   //offset : 6
    //padding 7
    
    //SIZE : 8Byte
}

 

즉, 중첩되더라도 자리차지는 그냥 하나의 필드처럼 차지한다. 정렬 단위만 맞춰주는 개념이다. 이렇게 하면 Inner와 Outer의 Pack 사이즈가 달라도 문제 없다.

 

필드 내부에 참조 타입이 있으면 기대와 다를 수 있다. (가령 Auto처럼 동작한다거나)

 

예제

더보기
[StructLayout(LayoutKind.Sequential)]
public struct PersonA
{
    public int Age;
    public string Name;
    public int Height;
}

[StructLayout(LayoutKind.Sequential)]
public struct PersonB
{
    public int Age;
    public int Height;
    public string Name;
}

class Program
{
    static void Main()
    {
        PersonA a = default;
        PersonB b = default;

        long offsetA = GetOffset(ref a, ref a.Age);
        long offsetB = GetOffset(ref b, ref b.Age);

        Console.WriteLine($"PersonA.Age offset: {offsetA}");
        Console.WriteLine($"PersonB.Age offset: {offsetB}");
        
        offsetA = GetOffset(ref a, ref a.Name);
        offsetB = GetOffset(ref b, ref b.Name);

        Console.WriteLine($"PersonA.Name offset: {offsetA}");
        Console.WriteLine($"PersonB.Name offset: {offsetB}");
        
        offsetA = GetOffset(ref a, ref a.Height);
        offsetB = GetOffset(ref b, ref b.Height);

        Console.WriteLine($"PersonA.Height offset: {offsetA}");
        Console.WriteLine($"PersonB.Height offset: {offsetB}");
        
        // 출력
        // PersonA.Age offset: 8
        // PersonB.Age offset: 8
        // PersonA.Name offset: 0
        // PersonB.Name offset: 0
        // PersonA.Height offset: 12
        // PersonB.Height offset: 12
    }

    static long GetOffset<T, TField>(ref T obj, ref TField field)
    {
        unsafe
        {
            byte* basePtr = (byte*)Unsafe.AsPointer(ref Unsafe.As<T, byte>(ref obj));
            byte* fieldPtr = (byte*)Unsafe.AsPointer(ref Unsafe.As<TField, byte>(ref field));
            return fieldPtr - basePtr;
        }
    }
}

 

 

Explicit

모든 필드에 대해 직접적으로 Offset을 지정한다. 내부 모든 필드에 [FieldOffset(n)] 어트리뷰트를 지정해야 한다.

 

[StructLayout(LayoutKind.Explicit)]
struct SomeUnion
{
    [FieldOffset(0)] public int A;
    [FieldOffset(0)] public float B;
    [FieldOffset(6)] public bool C;
    // [FieldOffset(0)] public string No; //타입을 로드할 때  System.TypeLoadException 발생함.

    //SIZE : 7
}

 

Padding도 없다. string과 같은 reference type에 필드 오프셋을 지정하면 익셉션이 발생한다. 당연하게도 reference type을 저렇게 쓰면 GC가 추적할 수 없기 때문이다.

 

해당 어트리뷰트를 이용해 C#에서도 union을 만들 수 있다. 자세한 것은 아래 활용 참고.

 

Class에서는 쓰지 않는 게 좋다. 그 이유는 C# 클래스에는 헤더(싱크블록 인덱스, 메서드테이블 주소)가 있기 때문이다. 그리고 GC등과 충돌할 수 있고 IL2CPP같은 AOT에서는 크래시난다.

 

 

Auto

class의 기본 값이다.

 

런타임에서 필드의 위치나 패딩등을 적절하게 알아서 넣어준다. 따라서 필드의 순서가 보장되지 않는다.

 

[StructLayout(LayoutKind.Auto)]
struct AutoLayout
{
    public byte B1;
    public int A;
    public byte B2;
    
    //SIZE : 8Byte (Sequential이라면 12Byte)
}

 

필드의 순서가 보장되지 않기 때문에 정렬, 패딩, 전체적인 크기등을 예측할 수 없어진다.

 

따라서 마샬링이나, interop 에서 사용이 불가하다. IL2CPP같은 코드 생성기에서도 레이아웃 결과가 균일하지 않을 수 있어진다. 

 

 

사이즈 지정

캐시라인의 크기인 64바이트에 맞춰 특정 구조체의 크기를 맞추고 싶을 수 있다. (Cpp의 alignas)

 

혹은 SIMD를 쓰는 경우에도 특정 구조체의 크기를 어느 값에 맞추고 싶을 수 있다.

 

이 경우 StructLayout에 Size값을 넘겨주면 된다.

[StructLayout(LayoutKind.Sequential, Size = 64)]
struct Person
{
    public int Age;
    
    //SIZE : 64
}

 

그러나 내부에 참조 필드가 있으면 이 값을 보장하지 않는다.

 

 

 

공용체

[StructLayout(LayoutKind.Explicit)]
struct ValueUnion
{
    [FieldOffset(0)] public int IntValue;
    [FieldOffset(0)] public byte ByteValue;
}

[StructLayout(LayoutKind.Sequential)]
struct FullUnion
{
    public ValueUnion Value;  // 정렬상 offset 0
    public string Text;       // 참조형 필드는 Sequential에서만 사용 가능
}

class Program
{
    static void Main()
    {
        var data = new FullUnion
        {
            Value = new ValueUnion { IntValue = 123456 },
            Text = "Hello Union"
        };

        Console.WriteLine($"Int:  {data.Value.IntValue}");
        Console.WriteLine($"Byte: {data.Value.ByteValue}");
        Console.WriteLine($"Text: {data.Text}");
    }
}

 

공용체 같은 걸 만들어볼 수도 있다. 이 때, string같은 참조형 필드는 레이아웃 지정이 불가하므로, Value type만 묶어서 써야한다.

 

 

 

'언어 > C#' 카테고리의 다른 글

성능 측정 - DotnetBenchmark 퀵세팅  (0) 2025.03.11
ThreadLocal<T>, AsyncLocal<T>  (1) 2024.12.20
ValueTask / ValueTask<T>  (0) 2024.05.02
Task.Delay (vs Thread.Sleep)  (2) 2024.04.10
동시성 개요  (0) 2024.04.01