[Day 1 / .NET Conf 2025] .NET 10의 성능 개선 | Stephen Toub


.NET 10에서 메모리 할당을 완전히 제거하고 성능을 최대 50배까지 향상시킨 혁신적인 최적화 기법들이 공개되었습니다. Escape Analysis를 통한 스택 할당, 컬렉션 열거 시 제로 할당, LINQ 쿼리 최적화로 밀리초를 나노초로 단축, 정규표현식 엔진의 알고리즘 개선 등 실제 벤치마크로 검증된 극적인 성능 개선이 모든 .NET 애플리케이션에 자동으로 적용됩니다.


.NET 10 성능 개선 하이라이트

개요

Microsoft .NET 팀의 Stephen Toub이 .NET 10의 성능 개선 사항을 발표했습니다. 약 300개의 성능 관련 PR이 포함되었으며, 이 중 25%는 커뮤니티 기여입니다. 블로그 포스트는 PDF로 출력 시 약 250페이지에 달하며, .NET 역사상 가장 빠른 릴리스로 평가됩니다.

1. Deabstraction (추상화 제거)

1.1 Escape Analysis와 Stack Allocation

.NET 10은 향상된 Escape Analysis를 통해 객체가 메서드 외부로 "탈출"하지 않는 경우 스택 할당을 수행합니다.

Stopwatch 예제:

  • .NET Framework 4.8: 50ns, 40 bytes 할당
  • .NET 9: 40ns, 40 bytes 할당
  • .NET 10: 30ns, 0 bytes 할당

String 배열 예제:

string[] array = new[] { a, b };
foreach (var value in array) {
    sum += value.Length;
}
  • .NET Framework 4.8: 10-15ns, 40 bytes
  • .NET 9: 8-10ns, 40 bytes
  • .NET 10: 4-5ns, 0 bytes

1.2 컬렉션 열거 최적화

IEnumerable 열거

배열을 IEnumerable로 처리할 때:

  • .NET Framework 4.8: 500ns, 32 bytes
  • .NET 9: 190ns, 32 bytes
  • .NET 10: 약 40ns, 0 bytes (약 4-5배 빠름)

List 열거

List를 IEnumerable로 처리할 때:

  • .NET Framework 4.8: 700ns, 40 bytes
  • .NET 9: 200ns, 40 bytes
  • .NET 10: 약 100ns, 0 bytes (약 2배 빠름)

Stack 열거

강타입 열거자 사용:

  • .NET Framework 4.8: 500ns
  • .NET 9: 250ns
  • .NET 10: 50ns (10배 빠름)

IEnumerable로 추상화:

  • .NET Framework 4.8: 800ns, 40 bytes
  • .NET 9: 300ns, 40 bytes
  • .NET 10: 50-60ns, 0 bytes

Queue 열거

  • .NET Framework 4.8: 900ns, 40 bytes
  • .NET 9: 350ns, 40 bytes
  • .NET 10: 약 100ns, 0 bytes (9배 빠름)

ConcurrentDictionary<TKey, TValue> 열거

  • .NET Framework 4.8: 1.6-1.7μs, 56 bytes
  • .NET 9: 900ns, 56 bytes
  • .NET 10: 140ns, 0 bytes

1.3 BitArray 개선

Hamming Distance 계산 예제:

BitArray에서 백업 스토어에 직접 접근할 수 있는 CollectionsMarshal.AsBytes 메서드가 추가되어 벡터화 연산 활용이 가능해졌습니다.

#if NET10_0_OR_GREATER
return TensorPrimitives.HammingBitDistance(
    CollectionsMarshal.AsBytes(bits1),
    CollectionsMarshal.AsBytes(bits2)
);
#endif
  • .NET Framework 4.8: 50,000ns
  • .NET 9: 16,000ns (3배 빠름)
  • .NET 10: 10ns (5,000배 빠름, Vector 128/256/512 활용)

2. LINQ 최적화

2.1 쿼리 연산자 간 정보 전달

LINQ는 쿼리 연산자 간 정보 전달을 통해 불필요한 연산을 제거합니다.

OrderBy().First() 최적화

.NET Core 3.0 이후: OrderBy + First는 전체 정렬 대신 Min 연산으로 변환

values.OrderBy(x => -x).First();
  • .NET Framework 4.8: 122ms
  • .NET 9: 2ms (60배 빠름)

OrderBy().Contains() 최적화 (신규)

.NET 10에서 Contains도 최적화 인식:

values.OrderBy(x => -x).Contains(value);
  • .NET Framework 4.8: 128ms
  • .NET 9: 83ms
  • .NET 10: 10-20ns (수천 배 빠름)

Reverse().Contains() 최적화 (신규)

.NET 10은 Reverse가 Contains 결과에 영향 없음을 인식하고 완전히 생략:

values.Reverse().Contains(value);
  • .NET Framework 4.8: 8MB 할당
  • .NET 9: 4MB 할당
  • .NET 10: 10ns 미만, 0 bytes

2.2 새로운 LINQ 메서드

  • Shuffle()
  • LeftJoin()
  • RightJoin()

3. 정규표현식 (Regex) 엔진 개선

3.1 Regex Source Generator

.NET 7에서 도입된 Regex Source Generator컴파일 타임 또는 IDE 실시간으로 정규표현식을 C# 코드로 생성합니다.

빈 Regex:

new Regex("")

→ 항상 성공하는 빈 문자열 매칭 코드 생성

패턴 ABCF 또는 DEF:

new Regex("ABC|DEF")

→ 통계적으로 덜 흔한 문자(C, F)를 먼저 찾는 최적화된 코드 생성

3.2 Atomic Loop 최적화

Greedy Loop vs Atomic Loop

Greedy Loop (a[ab]):*

  • 최대한 많은 'a’를 소비
  • 매칭 실패 시 백트래킹 수행
  • 라벨과 점프 코드 생성

Atomic Loop (a*b):

  • .NET 10에서 a*가 b와 겹칠 수 없음을 인식
  • 백트래킹 불필요
  • 라벨과 점프 코드 제거
  • 알고리즘 복잡도 감소

Unicode 카테고리 추론 (신규)

.NET 9 이전:

\w+\p{Sm}

→ Greedy loop (단어 문자와 수학 기호가 겹치는지 판단 불가)

.NET 10:

\w+\p{Sm}

→ Atomic loop (수학 기호는 단어 문자가 아님을 인식)
“Match a character in the set…”“Match Unicode Math Symbol” (가독성 향상)

공백 문자 추론:

\s\S
  • .NET 9: Greedy
  • .NET 10: Atomic (공백과 비공백은 상호 배타적)

3.3 Look-ahead에서 Anchor 추출

Zero-width positive look-ahead 최적화:

(?=^hello)

.NET 9 이전:

  • 입력 전체에서 “hello” 문자열 검색
  • 각 위치에서 매칭 시도

.NET 10:

^hello
  • Anchor(^)를 look-ahead 밖으로 추출
  • 문자열 시작 부분만 검사
  • 불필요한 검색 완전 제거

실제 성능 (Mark Twain 전집 텍스트):

  • .NET Framework 4.8: 24ms
  • .NET 9: 2ms (12배 빠름)
  • .NET 10: 30-40ns (600배 이상 빠름)

3.4 분석기 (Analyzer) 개선

.NET 10은 코드 분석기를 통해 개선 기회를 제안합니다.

예: Regex.Matches().Count() → Regex.Count()

// Before
regex.Matches(input).Count();

// .NET 10 제안
regex.Count(input);

개선 효과:

  • 할당 제거 (allocation-free)
  • 훨씬 적은 작업량

실용적인 팁

벤치마킹 방법

권장: BenchmarkDotNet 사용

  • 통계적 분석
  • 이상치 제거
  • 나노초 수준 정확도

데모용 간단한 방법:

var before = GC.GetAllocatedBytesForCurrentThread();
var sw = Stopwatch.StartNew();
// 테스트 코드 반복
sw.Stop();
var after = GC.GetAllocatedBytesForCurrentThread();
// 시간과 메모리 차이 출력

SharpLab 활용

SharpLab을 통해 실시간으로 IL 코드 확인:

  • C# 컴파일러의 최적화 확인
  • foreach 루프가 for 루프로 변환되는지 확인
  • 열거자 할당 여부 확인

.NET 버전별 조건부 컴파일

#if NET10_0_OR_GREATER
    // .NET 10 전용 최적화 코드
    return TensorPrimitives.HammingBitDistance(...);
#else
    // 폴백 코드
#endif

주요 성능 개선 요약

기능 .NET Framework 4.8 .NET 9 .NET 10 개선 비율
Stopwatch 할당 50ns, 40 bytes 40ns, 40 bytes 30ns, 0 bytes 제로 할당
IEnumerable 열거 (배열) 500ns, 32 bytes 190ns, 32 bytes 40ns, 0 bytes 12배
Stack 열거 (직접) 500ns 250ns 50ns 10배
ConcurrentDictionary 열거 1,700ns, 56 bytes 900ns, 56 bytes 140ns, 0 bytes 12배
BitArray Hamming 50,000ns 16,000ns 10ns 5,000배
LINQ OrderBy().Contains() 128ms 83ms 20ns 수천 배
Regex 앵커 최적화 24ms 2ms 40ns 600배

핵심 기술

  1. Escape Analysis: 객체가 메서드를 벗어나지 않으면 스택 할당
  2. Devirtualization: 가상 메서드 호출을 직접 호출로 변환
  3. Inlining: 메서드 인라인화 (try-finally 블록도 .NET 10부터 가능)
  4. Vectorization: SIMD 명령어 활용 (Vector128/256/512)
  5. Query Operator Fusion: LINQ 연산자 간 정보 전달 및 결합
  6. Atomic Loop Conversion: 백트래킹 불필요한 루프 최적화

주의사항

  • 모든 애플리케이션은 .NET 10으로 업그레이드만 해도 자동으로 성능 향상 적용
  • 일부 최적화는 특정 패턴에서만 작동 (예: IEnumerable 대신 구체 타입 사용)
  • HTTP 클라이언트 인스턴스는 캐싱 필요 (데모에서는 예외)
  • 벤치마킹 시 통계적 분석이 중요 (BenchmarkDotNet 권장)

커뮤니티 기여

.NET 10 성능 개선의 25%는 오픈소스 커뮤니티의 기여로 이루어졌습니다. Microsoft는 사용자들의 피드백(개선 사항 및 회귀 발견)을 통해 .NET 11을 더욱 향상시킬 계획입니다.

6개의 좋아요