sizeof 연산자 - 타입이 차지하는 메모리 크기 구하기
sizeof 연산자 - 타입이 차지하는 메모리 크기 구하기
sizeof 연산자는 특정 타입의 변수 하나가 차지하는 바이트 수를 알려줘요. 안전 코드에서는 크기가 플랫폼에 따라 달라지지 않는 기본 제공 비관리 형식의 이름이나 열거형 형식만 인자로 쓸 수 있어요. 이 연산자는 C# 1.0부터 있었던 아주 오래된 기능이죠.
출처: Microsoft Learn
본문
아래 표에 나오는 식들은 컴파일 타임에 해당 상수 값으로 평가돼요. 즉 실행할 때 계산되는 게 아니라, 미리 정해진 값이 그대로 박혀 들어간다 보면 돼요. 그래서 이런 경우에는 unsafe 문맥이 필요 없어요:
| 식 | 상수 값 |
|---|---|
sizeof(sbyte) |
1 |
sizeof(byte) |
1 |
sizeof(short) |
2 |
sizeof(ushort) |
2 |
sizeof(int) |
4 |
sizeof(uint) |
4 |
sizeof(long) |
8 |
sizeof(ulong) |
8 |
sizeof(char) |
2 |
sizeof(float) |
4 |
sizeof(double) |
8 |
sizeof(decimal) |
16 |
sizeof(bool) |
1 |
표에 나온 타입들의 크기는 전부 컴파일 타임 상수예요. 그래서 실제로 메모리를 훑어볼 필요 없이 컴파일러가 바로 값을 알아요.
열거형 형식에 sizeof를 적용하면 그 열거형의 기반 정수 타입(underlying integral type) 크기가 나와요. 이것도 컴파일 타임에 계산돼요.
한편 unsafe 코드 안에서는 규칙이 좀 더 넓어져요. void만 빼고 아무 타입에나 sizeof를 쓸 수 있어요. 심지어 타입 매개변수로 만든 타입에도요.
[!NOTE] C# 15부터 추가되는 memory safety 미리보기 기능을 쓰면,
unsafe문맥 밖에서도 아무 비관리 형식에sizeof를 적용할 수 있게 돼요. 다만 이건 아직 미리보기 기능이니 참고만 해두세요.
몇 가지 크기 규칙을 정리해볼게요:
- 참조 타입이나 포인터 타입의 크기는 그 참조·포인터 자체의 크기예요. 그것이 가리키는 객체의 크기가 아니에요.
- 값 타입(비관리 형식이든 아니든)의 크기는 그 값 하나의 크기예요.
- 열거형 타입의 크기는 기반 정수 타입의 크기예요. 이 값은 컴파일 타임 상수이죠. 여기서 주의할 점이 하나 있어요. 참조된 어셈블리에 정의된 열거형의 기반 타입이 나중에 바뀌면, 그 열거형에
sizeof를 적용한 코드는 새로운 크기를 반영하려면 다시 컴파일해야 해요. ref struct타입의 크기는 그 값의 크기예요.ref필드 몇 개가 있다면, 각 필드의 크기는 그 필드가 가리키는 값이 아니라 참조·포인터 자체의 크기로 잡혀요.
실제로 어떻게 쓰는지 예시로 확인해볼게요. 여기서는 Point라는 구조체를 만들고, 기본 타입들뿐 아니라 unsafe 문맥에서 포인터와 제네릭 타입의 크기도 찍어봐요:
public struct Point
{
public Point(byte tag, double x, double y) => (Tag, X, Y) = (tag, x, y);
public byte Tag { get; }
public double X { get; }
public double Y { get; }
}
public class SizeOfOperator
{
public static void Main()
{
Console.WriteLine(sizeof(byte)); // output: 1
Console.WriteLine(sizeof(double)); // output: 8
DisplaySizeOf<Point>(); // output: Size of Point is 24
DisplaySizeOf<decimal>(); // output: Size of System.Decimal is 16
unsafe
{
Console.WriteLine(sizeof(Point*)); // output: 8
Console.WriteLine(sizeof(nint)); // output: 8 on 64-bit, 4 on 32-bit
Console.WriteLine(sizeof(nuint)); // output: 8 on 64-bit, 4 on 32-bit
Console.WriteLine(sizeof(Span<int>)); // output: 16 on 64-bit, 12 on 32-bit
}
}
static unsafe void DisplaySizeOf<T>() where T : unmanaged
{
Console.WriteLine($"Size of {typeof(T)} is {sizeof(T)}");
}
}
sizeof가 돌려주는 값은 공용 언어 런타임(CLR)이 관리되는 메모리에서 실제로 할당한 바이트 수예요. struct 타입의 경우 그 값에는 패딩(padding)이 포함돼요. 위 예시에서 Point가 24바이트로 나오는 이유가 바로 그거예요 — byte(1)와 double(8)·double(8)이 실제로는 정렬 때문에 여유 공간을 두고 배치되거든요. 반면에 못 보던 메서드가 하나 더 있을 텐데, 바로 Marshal.SizeOf예요.
[!IMPORTANT]
sizeof와 xref:System.Runtime.InteropServices.Marshal.SizeOf(System.Object)?displayProperty=nameWithType의 결과는 같지 않을 수 있어요.Marshal.SizeOf는 타입이 비관리 메모리에서 차지하는 크기를 돌려주거든요. 관리 메모리 크기와 비관리 메모리 크기는 레이아웃 규칙이 달라서 다를 수 있으니, 목적에 맞는 쪽을 골라 써야 해요.
더 알아보기
- C# 연산자와 식
- 포인터 관련 연산자
- 포인터 형식
- 메모리 및 범위 관련 형식
- .NET의 제네릭
- C# 언어 사양의
sizeof연산자 섹션