.NET 10의 성능 개선 | Stephen Toub


.NET 10 성능 개선 사항 상세 요약

개요

.NET 10의 성능 개선은 단일한 큰 변화가 아닌, 수백 개의 작은 최적화들이 복합적으로 작용한 결과입니다. 이는 19세기 얼음 무역왕 Frederic Tudor의 사례와 같이, 각각의 작은 개선사항들이 서로 배가되어 전체적으로 혁신적인 성능 향상을 만들어냅니다.

벤치마킹 환경 설정

기본 설정

  • 사용 도구: BenchmarkDotNet 0.15.2
  • 테스트 환경: Linux Ubuntu 24.04.1 LTS, 11th Gen Intel Core i9-11950H 2.60GHz
  • 비교 대상: .NET 9.0과 .NET 10.0

프로젝트 설정 방법

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFrameworks>net10.0;net9.0</TargetFrameworks>
    <LangVersion>Preview</LangVersion>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <ServerGarbageCollection>true</ServerGarbageCollection>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="BenchmarkDotNet" Version="0.15.2" />
  </ItemGroup>

</Project>

벤치마크 실행 명령

dotnet run -c Release -f net9.0 --filter "*" --runtimes net9.0 net10.0

주의사항: 마이크로벤치마크는 매우 짧은 시간의 작업을 측정하므로, 하드웨어, 운영체제, 시스템 상태에 따라 결과가 달라질 수 있습니다.

JIT(Just-In-Time) 컴파일러 개선사항

1. 추상화 제거(Deabstraction)

객체 스택 할당(Object Stack Allocation)

.NET 10의 핵심 개선 사항 중 하나는 확장된 escape analysis를 통한 객체의 스택 할당입니다.

Escape Analysis 원리:

  • 메서드 내에서 할당된 객체가 메서드 밖으로 "탈출"하는지 분석
  • 탈출하지 않는 객체는 힙 대신 스택에 할당 가능
  • 스택 할당은 포인터 이동만으로 처리되며 GC 압박 감소

주요 개선사항:

  1. 델리게이트 관련 개선 (dotnet/runtime#115172)

    • 델리게이트의 Invoke 메서드가 this 참조를 보관하지 않음을 인식
    • 결과: 델리게이트 할당이 사실상 제거됨

    성능 비교 예시:

    Method    Runtime    Mean       Ratio    Allocated    Alloc Ratio
    Sum       .NET 9.0   19.530 ns  1.00     88 B         1.00
    Sum       .NET 10.0  6.685 ns   0.34     24 B         0.27
    
  2. 배열 스택 할당 (dotnet/runtime#104906, #112250)

    • 메서드 내에서만 사용되는 배열을 스택에 할당

    성능 비교 예시:

    Method    Runtime    Mean       Ratio    Allocated    Alloc Ratio
    Test      .NET 9.0   11.580 ns  1.00     48 B         1.00
    Test      .NET 10.0  3.960 ns   0.34     -            0.00
    
  3. Span과 배열 조합 최적화 (dotnet/runtime#113977, #116124)

    • 구조체 필드에 대한 escape analysis 지원
    • Span은 ref T 필드와 int length 필드를 가진 구조체로 처리

    성능 비교 예시:

    Method    Runtime    Mean        Ratio    Allocated    Alloc Ratio
    Test      .NET 9.0   9.7717 ns   1.04     32 B         1.00
    Test      .NET 10.0  0.8718 ns   0.09     -            0.00
    

2. 가상화 해제(Devirtualization)

배열 인터페이스 구현 개선

주요 문제점: 배열의 인터페이스 구현은 다른 모든 인터페이스 구현과 다르게 처리되어 JIT가 가상화 해제를 적용하지 못했습니다.

해결책 (dotnet/runtime#108153, #109209, #109237, #116771):

  • 배열의 인터페이스 메서드 구현에 대한 가상화 해제 지원

실제 영향:

  • ReadOnlyCollection에서 foreach가 for 루프보다 빨랐던 이상한 현상 해결
  • LINQ의 IList 특화 코드 경로가 실제로 최적화로 작동

성능 비교:

Method          Runtime    Mean        Ratio
SumEnumerable   .NET 9.0   968.5 ns    1.00
SumEnumerable   .NET 10.0  775.5 ns    0.80
SumForLoop      .NET 9.0   1,960.5 ns  1.00
SumForLoop      .NET 10.0  624.6 ns    0.32

Guarded Devirtualization (GDV) 개선

개선사항 (dotnet/runtime#116453, #109256):

  • 동적 PGO를 통한 타입별 특화 코드 생성
  • 공유 제네릭 컨텍스트에서도 GDV 적용

성능 비교:

Method    Runtime    Mean      Ratio
Test      .NET 9.0   2.816 ns  1.00
Test      .NET 10.0  1.511 ns  0.54

3. 경계 검사(Bounds Checking) 최적화

기본 개념

C#은 메모리 안전 언어로, 배열, 문자열, span의 경계를 벗어나는 접근을 방지합니다. 하지만 성능을 위해 불필요한 경계 검사는 제거해야 합니다.

주요 개선사항

  1. 수학적 연산 결과 보장 (dotnet/runtime#109900)

    • BitOperations.Log2 같은 연산에서 결과값이 보장된 범위 내에 있을 때 경계 검사 제거
  2. Log2 최대값 인식 (dotnet/runtime#113790)

    • JIT가 Log2 연산의 최대 가능값을 이해하여 경계 검사 최적화

어셈블리 코드 비교:

; .NET 9 (경계 검사 포함)
call CORINFO_HELP_RNGCHKFAIL
int 3
; Total bytes of code 74

; .NET 10 (경계 검사 제거)
ret
; Total bytes of code 58

4. 복제(Cloning) 최적화

개선 영역:

  • 객체 복제 시 불필요한 메모리 할당 제거
  • 깊은 복사와 얕은 복사 구분을 통한 최적화

5. 인라이닝(Inlining) 개선

주요 개선사항 (dotnet/runtime#110827):

  • 가상화 해제 이후 추가 인라이닝 기회 탐색
  • 다단계 최적화 프로세스를 통한 더 많은 메서드 인라이닝

6. 상수 폴딩(Constant Folding)

개선 내용:

  • 컴파일 타임에 상수값으로 계산 가능한 표현식들을 미리 계산
  • 런타임 계산 부하 감소

7. 코드 레이아웃(Code Layout) 최적화

개선 영역:

  • 자주 실행되는 코드 경로를 CPU 캐시에 유리하게 배치
  • 분기 예측 성능 향상

8. GC 쓰기 장벽(Write Barriers) 최적화

개선 내용:

  • 가비지 컬렉터의 쓰기 장벽 오버헤드 감소
  • 메모리 참조 추적 효율성 향상

9. 명령어 집합(Instruction Sets) 활용

개선 영역:

  • 최신 CPU 명령어 집합 활용도 증가
  • SIMD 명령어 최적화

10. 기타 최적화

포함 영역:

  • 메모리 접근 패턴 최적화
  • 레지스터 할당 개선
  • 분기 최적화

Native AOT 개선사항

주요 특징:

  • Ahead-of-Time 컴파일을 통한 시작 시간 단축
  • 메모리 사용량 감소
  • 독립 실행 파일 생성

VM(Virtual Machine) 개선사항

스레딩(Threading) 최적화

  • 스레드 생성 및 관리 효율성 향상
  • 동기화 프리미티브 성능 개선

리플렉션(Reflection) 성능 향상

  • 메타데이터 접근 속도 개선
  • 동적 메서드 호출 최적화

프리미티브 및 수치 연산(Primitives and Numerics)

개선 영역:

  • 기본 데이터 타입 연산 최적화
  • 수학 함수 성능 향상
  • 비트 연산 효율성 증대

컬렉션(Collections) 개선사항

열거(Enumeration) 최적화

  • foreach 루프 성능 향상
  • 열거자 할당 감소

LINQ 성능 개선

  • 쿼리 실행 최적화
  • 메모리 할당 감소

Frozen Collections

  • 불변 컬렉션의 성능 최적화
  • 읽기 전용 시나리오에서의 효율성 향상

BitArray 개선

  • 비트 연산 성능 향상
  • 메모리 사용량 최적화

기타 컬렉션 최적화

  • Dictionary, List, HashSet 등의 성능 개선
  • 캐시 지역성 향상

I/O 및 네트워킹

I/O 성능 개선

  • 파일 읽기/쓰기 최적화
  • 비동기 I/O 효율성 향상

네트워킹 최적화

  • HTTP 클라이언트 성능 개선
  • 소켓 통신 효율성 증대

검색 및 문자열 처리

정규식(Regex) 성능 향상

  • 패턴 매칭 알고리즘 최적화
  • 컴파일된 정규식 성능 개선

SearchValues 최적화

  • 문자열 검색 성능 향상
  • SIMD 명령어 활용

MemoryExtensions 개선

  • 메모리 조작 함수 최적화
  • Span과 Memory 작업 효율성 향상

JSON 처리 성능

개선 영역:

  • System.Text.Json 성능 향상
  • 직렬화/역직렬화 속도 개선
  • 메모리 할당 감소

진단(Diagnostics) 도구

개선 내용:

  • 성능 분석 도구 정확도 향상
  • 프로파일링 오버헤드 감소

암호화(Cryptography)

개선 영역:

  • 암호화 알고리즘 성능 향상
  • 하드웨어 가속 활용 증대

실용적 팁 및 주의사항

성능 최적화 활용 팁

  1. 객체 할당 최소화

    • 지역 변수로 제한된 객체들은 자동으로 스택 할당될 수 있음
    • 불필요한 클로저 생성 피하기
  2. 배열과 Span 활용

    • 작은 배열의 경우 stackalloc 고려
    • Span를 활용한 메모리 효율적 코드 작성
  3. 인터페이스 사용 최적화

    • 구체적인 타입이 알려진 경우 가상화 해제 혜택 활용
    • ReadOnlyCollection보다 구체적인 컬렉션 타입 사용 고려

주의사항

  1. 마이크로벤치마크 해석

    • 실제 애플리케이션에서의 성능과 차이가 있을 수 있음
    • 전체적인 애플리케이션 성능 측정이 중요
  2. 하드웨어 의존성

    • CPU 아키텍처에 따른 성능 차이 존재
    • 타겟 환경에서의 실제 테스트 필요
  3. 메모리 안전성

    • 성능 향상이 메모리 안전성을 해치지 않음
    • C#의 메모리 안전 특성 유지

코드 예제

스택 할당 최적화 예제

// .NET 10에서 자동으로 스택 할당됨
public int ProcessArray()
{
    var array = new int[] { 1, 2, 3 }; // 스택 할당
    return ProcessData(array);
}

private static int ProcessData(int[] data)
{
    return data.Sum(); // 인라인되어 배열이 탈출하지 않음
}

Span 활용 예제

// .NET 10에서 할당 없이 처리
public void CopyBytes(int value, Span<byte> destination)
{
    var bytes = BitConverter.GetBytes(value); // 스택 할당
    bytes.AsSpan().Slice(0, 3).CopyTo(destination);
}

가상화 해제 활용 예제

// .NET 10에서 가상 호출이 직접 호출로 최적화됨
public int SumArray(int[] array)
{
    int sum = 0;
    for (int i = 0; i < array.Length; i++)
    {
        sum += array[i]; // 가상화 해제된 배열 접근
    }
    return sum;
}

학습 리소스 및 참고 자료

공식 문서 및 소스코드

  • GitHub Issues: dotnet/runtime 저장소의 성능 관련 이슈들
    • dotnet/runtime#115172: 델리게이트 escape analysis
    • dotnet/runtime#104906, #112250: 배열 스택 할당
    • dotnet/runtime#113977, #116124: Span 필드 분석
    • dotnet/runtime#108153, #109209, #109237, #116771: 배열 인터페이스 가상화 해제

이전 버전 성능 개선 시리즈

  • Performance Improvements in .NET 9
  • Performance Improvements in .NET 8
  • Performance Improvements in .NET 7
  • Performance Improvements in .NET 6
  • Performance Improvements in .NET 5
  • Performance Improvements in .NET Core 3.0
  • Performance Improvements in .NET Core 2.1
  • Performance Improvements in .NET Core 2.0

벤치마킹 도구

  • BenchmarkDotNet: .NET 성능 측정을 위한 표준 라이브러리
  • dotnet-counters: 런타임 성능 카운터 모니터링
  • PerfView: 메모리 및 CPU 사용량 분석

다음 단계 및 미래 전망

.NET 10의 성능 개선사항들은 다음과 같은 방향으로 발전할 예정입니다:

  1. 더 정교한 Escape Analysis: 클로저 객체 할당 제거
  2. 확장된 가상화 해제: 더 많은 시나리오에서의 최적화
  3. 하드웨어 특화 최적화: 최신 CPU 기능 활용 확대
  4. 컴파일 타임 최적화: AOT 컴파일 성능 향상

이러한 개선사항들은 개발자가 코드를 변경하지 않아도 자동으로 적용되며, .NET 생태계 전반의 성능을 향상시킵니다.