Lobsters

Performance Improvements in .NET 11

.NET 11 성능 개선 총정리 — 매년 이어지는 JIT·런타임 최적화 리포트

Microsoft가 공개한 .NET 11의 연례 성능 개선 보고서로, JIT 컴파일러의 탈추상화(deabstraction)·가드드 디버추얼라이제이션(GDV)·이스케이프 분석(escape analysis) 등 수백 건의 최적화를 BenchmarkDotNet 마이크로 벤치마크와 함께 소개합니다. 대부분의 개선은 애플리케이션 코드 수정 없이 런타임 업그레이드만으로 이점을 얻을 수 있다는 점이 핵심입니다.

본문

Stephen Toub가 매년 집필하는 .NET 성능 개선 시리즈의 .NET 11 편입니다. 제목은 영화 This Is Spinal Tap의 유명한 대사("this one goes to eleven")에서 따온 것으로, 단순한 마케팅 문구가 아니라 bounds check 제거, 할당 제거, 락 회피 같은 실측 가능한 개선이 누적된 결과라고 저자는 강조합니다. 본문은 BenchmarkDotNet으로 재현 가능한 수백 개의 마이크로 벤치마크로 구성되며, .NET 10과 .NET 11을 멀티 타기팅해 나란히 비교할 수 있도록 csproj 설정과 실행 명령(dotnet run -c Release -f net10.0 --runtimes net10.0 net11.0)까지 함께 제공합니다.

JIT: 가장 광범위한 영향을 주는 최적화 지점

관리 코드는 결국 JIT 컴파일러가 네이티브 명령으로 번역하기 때문에, JIT 한 곳의 개선이 그 패턴이 등장하는 모든 애플리케이션·라이브러리 코드에 적용됩니다. 핵심 개념은 '탈추상화'로, 가상 호출의 실제 대상을 찾아내는 디버추얼라이제이션(devirtualization), 힙 할당이 스코프를 벗어나지 않음을 증명하는 이스케이프 분석 등이 포함됩니다. 글은 Animal/Dog/Cat 예제로 가상 디스패치가 왜 비싼지를 어셈블리 수준에서 보여줍니다 — 메서드 테이블 포인터 로딩, vtable 인덱싱, 간접 호출이라는 세 단계 의존 메모리 참조가 필요하고, 호출 대상을 모르면 인라이닝도 불가능해 상수 전파·데드 코드 제거 같은 후속 최적화 기회까지 잃습니다.

정적 분석으로 타입을 증명할 수 없을 때는 프로필 기반 최적화(PGO)와 계층 컴파일(tiered compilation)이 동작합니다. Tier 0에서 수집한 프로필로 Tier 1 재컴파일 시 '가드드 디버추얼라이제이션(GDV)'을 수행하는데, 이는 "animal.GetType() == typeof(Dog)이면 직접 호출, 아니면 원래 가상 호출" 같은 런타임 검증 코드를 삽입하는 방식입니다. 실제 워크로드의 큰 처리량 향상 상당수가 이 패턴에서 나오며, 인터페이스 디스패치에도 적용됩니다.

이스케이프 분석: 스택 할당과 할당 제거

.NET 11은 객체가 메서드를 '탈출'하지 않는다고 증명할 수 있는 패턴을 더 넓혔습니다. 첫째, nullable boxing이 JIT 내부로 노출되어 이스케이프 분석이 적용됩니다 — Nullable<int>를 IFormattable로 캐스팅해 ToString을 호출하는 코드에서 임시 24바이트 박스 할당이 사라져 9.583ns → 1.987ns(비율 0.21)로 단축됐습니다. 둘째, .NET 10에 도입된 조건부 이스케이프 분석(CEA)이 확장되어, GetEnumerator()가 다른 GetEnumerator() 결과를 위임하는 '체이닝' 패턴까지 처리합니다. 컬렉션 식([1,2,3,4,5])을 IEnumerable<int> 인스턴스 필드에 둔 foreach에서 32바이트 열거자 할당이 제거되어 13.874ns → 2.674ns(비율 0.19)로 개선됐고, static readonly 필드와 동일한 수준에 도달했습니다. 셋째, IL의 constrained. 접두사가 수신자를 간접 읽기로 표현해 주소 노출로 오인하던 문제를 수정해, EqualityComparer<T>.Default.Equals 경로에서 24바이트 할당이 사라졌습니다(3.874ns → 1.786ns).

그 밖의 JIT 개선

ArgumentNullException.ThrowIfNull(value)에 struct를 넘길 때 발생하던 박싱이 최적화 코드에서는 이미 제거됐지만 Tier 0에서는 남아 있던 문제가 .NET 11에서 해소돼, 시작 단계와 프로파일링 노이즈가 줄었습니다. 제네릭 가상 메서드(GVM) 쪽으로는 ldvirtftn 호출 대상을 조기에 임시 변수에 쏟지 않게 하고, 비공유 GVM의 디버추얼라이제이션을 가능하게 하며, 공유 GVM과 기본 인터페이스 구현까지 지원을 확장하는 다수의 PR이 반영됐습니다. 글 전체가 이처럼 '할당 하나, 검사 하나' 단위의 개선을 벤치마크 수치와 함께 나열하는 방식이라, .NET을 쓰는 개발자에게는 런타임이 실제로 어떤 코드를 빠르게 만드는지 이해하는 교재로도 쓸 만합니다.

원문: Microsoft .NET DevBlogs / 번역·요약: Trawling