← 문제로

52. string 에 대한 switch 문의 컴파일러 변환 (해시 점프 vs 순차 비교)

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

해설 — string 에 대한 switch 문의 컴파일러 변환 (해시 점프 vs 순차 비교)

난이도: 중

요약

C# 컴파일러는 string switch를 케이스 수에 따라 두 전략으로 낮춘다. 케이스가 적으면(대략 6개 이하) 단순한 if/else 순차 문자열 비교(==, 서수 비교) 체인으로, 케이스가 많으면 각 문자열의 해시를 계산해 그 해시값에 대한 정수 switch(점프 테이블) 로 분기한 뒤 각 후보에서 서수 == 로 최종 확인한다. 문자열 비교는 항상 서수(ordinal) 기준이라 "ROUTE"와 "route"는 서로 다른 케이스다(문화권/대소문자 무시가 아님).

핵심 개념

  • string은 정수처럼 곧바로 점프 테이블을 만들 수 없다(값이 연속이 아님). 그래서:
    • 소수 케이스: if (op == "move") ... else if (op == "attack") .... 비교 횟수 최악 O(n).
    • 다수 케이스: 문자열 해시 → 해시값으로 정수 switch(점프 테이블) → 해당 후보에서 서수 == 재확인. 실질 O(1)에 가깝다.
  • 문자열 동등 비교는 string.Equals의 서수 의미(코드 유닛 단위 정확 일치). 대소문자·문화권 규칙을 타지 않는다.

단계별 내부 동작 (RouteMany, Roslyn 기준)

  1. 컴파일러가 <PrivateImplementationDetails>에 ComputeStringHash 헬퍼(비암호화 FNV류 해시)를 생성한다. 런타임 Dictionary 인스턴스를 만드는 게 아니다 — 해시 계산 + 정수 switch로 낮춘다. (참고: 옛 pre-Roslyn csc는 케이스가 많을 때 정적 필드에 Dictionary<string,int>를 지연 생성하는 방식이었다. 현대 Roslyn은 해시-스위치 방식으로 바뀌었다.)
  2. 런타임: op의 해시를 ComputeStringHash로 계산.
  3. 그 uint 해시값에 대해 IL switch(점프 테이블) 로 분기해 후보 케이스 그룹으로 이동.
  4. 각 후보에서 op == "move" 처럼 서수 문자열 비교로 최종 확인(해시 충돌·우연한 일치를 걸러냄).
  5. 어디에도 안 맞으면 default(여기선 0).

RouteFew는 해시 없이 op == "move" / op == "attack" 두 번의 서수 비교 후 default.

흔한 오해·함정

  • "string switch는 대소문자를 무시한다" — 아니다. 서수 비교라 정확히 일치해야 한다. 대소문자 무시가 필요하면 op.ToLowerInvariant()로 정규화 후 switch(단 ToLower는 할당 발생)하거나 명시 비교를 쓴다.
  • "내부적으로 Dictionary를 만든다" — 현대 Roslyn은 해시-스위치로 낮추며 런타임 사전을 할당하지 않는다(옛 컴파일러의 동작과 혼동 주의).
  • 문화권 의존 걱정 — string switch는 서수라 문화권 불변이다(터키어 'I' 문제에서 자유). 반대로 "ß"↔"ss" 같은 문화권 동등은 매칭되지 않는다.
  • null 입력: string switch는 null을 정상 처리한다. null 케이스가 있으면 그 팔로, 없으면 default로 간다(NRE 아님).
  • 성능: 초고빈도 디스패치라면 해시+비교 비용을 감안해 정수 opcode(enum)로 바꿔 순수 정수 switch를 쓰는 편이 빠르다.

면접 포인트

  • string switch의 두 lowering 전략(순차 비교 vs 해시-스위치)과 임계 케이스 수.
  • 비교가 서수(ordinal)라는 점, 대소문자/문화권/null 처리.
  • 현대 Roslyn이 런타임 Dictionary가 아니라 ComputeStringHash+정수 switch로 낮춘다는 사실.