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]

sizeofxref:System.Runtime.InteropServices.Marshal.SizeOf(System.Object)?displayProperty=nameWithType의 결과는 같지 않을 수 있어요. Marshal.SizeOf는 타입이 비관리 메모리에서 차지하는 크기를 돌려주거든요. 관리 메모리 크기와 비관리 메모리 크기는 레이아웃 규칙이 달라서 다를 수 있으니, 목적에 맞는 쪽을 골라 써야 해요.

더 알아보기