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 |