10. CLR 제네릭의 런타임 구현(값 형식 특수화·코드 공유·박싱 회피)
난이도 상내 리뷰 · C#
해설 · C#
해설 — CLR 제네릭의 런타임 구현(값 형식 특수화·코드 공유·박싱 회피)
난이도: 상
요약
CLR 제네릭은 값 형식마다 전용 네이티브 코드로 특수화되고, 참조 형식들은
하나의 네이티브 코드(canonical)를 공유한다. 덕분에 List<int> 는 int 를
박싱 없이 인라인 저장하지만, 비제네릭 ArrayList 는 object 로 담아
박싱한다. EqualityComparer<T>.Default 는 값 형식이 IEquatable<T> 를
구현하면 박싱 없는 비교기를 골라 준다. 반대로 제약 없는 T 에서 a.Equals(b)
(object 시그니처)는 인자를 박싱한다.
각 지점 예측
- (A)
EqualityComparer<T>.Default.Equals(p1, p2)→ 박싱 없음.Point가IEquatable<Point>를 구현하므로Default가GenericEqualityComparer<Point>를 반환하고, 이는IEquatable<Point>.Equals(Point)(값 인자)를 호출한다. - (B)
a.Equals(b)(제약 없는 T) → 박싱 발생.T에 보이는Equals는object.Equals(object)뿐이라 인자b가object로 박싱된다. (수신자a는constrained.접두사 덕에 박싱 없이 호출되지만, 인자 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), 값 형식 무박싱이 가능.
단계별 내부 동작 — 박싱을 피하는 비교
EqualityComparer<T>.Default는 정적 캐시에서T에 맞는 비교기를 한 번 생성해 재사용한다.T가IEquatable<T>구현 값 형식이면 →GenericEqualityComparer<T>선택 →((IEquatable<T>)x).Equals(y)를 값 인자로 호출 → 박싱 0.T가 그 인터페이스를 구현 안 하면 →ObjectEqualityComparer<T>→object. Equals경로(박싱 가능). 그래서 값 형식엔IEquatable<T>+GetHashCode재정의가 성능상 중요하다.
흔한 오해·함정
- "제네릭이면 무조건 박싱이 없다" — 제약 없는
T에서object멤버 (Equals/ToString)를 호출하거나T를object/비제네릭 인터페이스로 올리면 박싱된다. - "제네릭은 자바처럼 타입 소거" — 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·키 구조체에 특히 중요).