← 문제로

10. CLR 제네릭의 런타임 구현(값 형식 특수화·코드 공유·박싱 회피)

난이도 상
내 리뷰 · C#
해설 · C#

해설 — CLR 제네릭의 런타임 구현(값 형식 특수화·코드 공유·박싱 회피)

난이도: 상

요약

CLR 제네릭은 값 형식마다 전용 네이티브 코드로 특수화되고, 참조 형식들은 하나의 네이티브 코드(canonical)를 공유한다. 덕분에 List<int>int박싱 없이 인라인 저장하지만, 비제네릭 ArrayListobject 로 담아 박싱한다. EqualityComparer<T>.Default 는 값 형식이 IEquatable<T> 를 구현하면 박싱 없는 비교기를 골라 준다. 반대로 제약 없는 T 에서 a.Equals(b) (object 시그니처)는 인자를 박싱한다.

각 지점 예측

  • (A) EqualityComparer<T>.Default.Equals(p1, p2)박싱 없음. PointIEquatable<Point> 를 구현하므로 DefaultGenericEqualityComparer<Point> 를 반환하고, 이는 IEquatable<Point>.Equals(Point)(값 인자)를 호출한다.
  • (B) a.Equals(b) (제약 없는 T)박싱 발생. T 에 보이는 Equalsobject.Equals(object) 뿐이라 인자 bobject 로 박싱된다. (수신자 aconstrained. 접두사 덕에 박싱 없이 호출되지만, 인자 b 는 박싱.)
  • (C) ArrayList.Add(42)박싱 발생. 시그니처가 Add(object)int 가 힙에 박싱된다.
  • (D) List<int>.Add(42)박싱 없음. List<int>int[] 를 내부 버퍼로 쓰는 특수화 코드라 값을 그대로 저장.
  • (E) Dictionary<int,string>[7]=...박싱 없음. 키 해시/비교에 EqualityComparer<int>.Default(특수화)를 써 int 를 박싱하지 않는다.

핵심 개념 — CLR 제네릭의 런타임 모델

  • 값 형식 인스턴스화 = 특수화: List<int>, List<double>, List<Point> 는 각각 별도의 네이티브 코드로 JIT 된다. 필드/배열이 해당 값 형식 크기에 맞게 인라인 배치되어 박싱·간접 참조가 없다. 대신 인스턴스화 종류만큼 코드가 생긴다 (코드 크기 ↑).
  • 참조 형식 인스턴스화 = 코드 공유: List<string>, List<object>, List<Player> 는 모두 같은 canonical 네이티브 코드를 공유한다. 참조는 크기(포인터)가 같기 때문. 타입별 정보는 숨은 타입 인자(MethodTable)로 런타임에 전달된다.
  • 이것이 자바 제네릭(타입 소거, 항상 박싱)과의 결정적 차이다. C# 제네릭은 런타임에 실재(reified) 하므로 typeof(T), default(T), 값 형식 무박싱이 가능.

단계별 내부 동작 — 박싱을 피하는 비교

  1. EqualityComparer<T>.Default 는 정적 캐시에서 T 에 맞는 비교기를 한 번 생성해 재사용한다.
  2. TIEquatable<T> 구현 값 형식이면 → GenericEqualityComparer<T> 선택 → ((IEquatable<T>)x).Equals(y)값 인자로 호출 → 박싱 0.
  3. T 가 그 인터페이스를 구현 안 하면 → ObjectEqualityComparer<T>object. Equals 경로(박싱 가능). 그래서 값 형식엔 IEquatable<T> + GetHashCode 재정의가 성능상 중요하다.

흔한 오해·함정

  • "제네릭이면 무조건 박싱이 없다" — 제약 없는 T 에서 object 멤버 (Equals/ToString)를 호출하거나 Tobject/비제네릭 인터페이스로 올리면 박싱된다.
  • "제네릭은 자바처럼 타입 소거" — C# 은 실재화. new T()(where T:new()), default(T), typeof(T) 가 작동.
  • 값 형식 제네릭 남용 시 코드 비대화: 수십 종 값 형식으로 인스턴스화하면 JIT 코드가 그만큼 늘어 I-cache 압박. 트레이드오프 인지.
  • 인터페이스로 제약한 값 형식 호출: where T : IComparable<T>constrained callvirt 로 박싱 없이 호출 가능 → 핫패스 제네릭의 정석.

면접 포인트

  • "왜 List<int> 는 빠르고 ArrayList 는 느린가"를 박싱 + 특수화 로 설명.
  • CLR 제네릭의 값 형식 특수화 vs 참조 형식 코드 공유, 그리고 자바 타입 소거와의 차이.
  • 값 형식에 IEquatable<T>/GetHashCode 를 구현해야 컬렉션·비교에서 박싱을 없앤다는 실무 지식(서버의 좌표·ID·키 구조체에 특히 중요).