이 페이지에는 자동 번역된 텍스트가 포함될 수 있습니다.

C# Native AOT 성능

.NET Native AOT 애플리케이션은 일반 관리형 코드와 비교할 때 얼마나 빠른가? AOT가 JIT보다 더 뛰어난가? Native AOT 애플리케이션은 어떻게 벤치마크하는가?

Native AOT 성능 비교

이 문서는 .NET의 Native AOT에 관한 시리즈의 일부입니다. Native AOT에 익숙하지 않다면 먼저 .NET에서 Native AOT 애플리케이션을 개발하는 방법 부분을 읽어 보십시오.

이 문서는 .NET과 Native AOT 성능을 비교합니다. 먼저 공식 Microsoft 벤치마크를 살펴봅니다. 이를 통해 단순한 ASP.NET 애플리케이션에 대한 서로 다른 .NET 배포 옵션을 비교할 수 있습니다.

그다음 BenchmarkDotNet과 hyperfine 도구를 사용해 자체 벤치마크를 실행하는 방법을 배웁니다. 이러한 벤치마크를 통해 자신의 환경에서 코드 속도를 측정할 수 있습니다.

ASP.NET 벤치마크

ASP.NET 팀은 성능 테스트를 위한 견고한 인프라를 유지합니다. 다양한 환경에서 여러 시나리오를 테스트합니다.

우리가 가장 관심 있는 것은 Native AOT 벤치마크입니다. 주요 정보 출처는 다음 PowerBI 대시보드입니다. 그 데이터는 테스트 애플리케이션, 배포 시나리오, 메트릭이라는 3개의 "고래"를 기반으로 합니다.

테스트 애플리케이션

벤치마크와 테스트 애플리케이션의 소스 코드는 aspnet/Benchmarks 저장소에서 찾을 수 있습니다.

Native AOT 벤치마크는 3가지 애플리케이션 유형을 비교합니다.

  • Stage1 - HTTP와 JSON을 기반으로 한 최소 API입니다. 애플리케이션 소스 코드는 /src/BenchmarksApps/BasicMinimalApi에 있습니다.
  • Stage1Grpc - gRPC를 기반으로 한 유사한 API입니다(/src/BenchmarksApps/Grpc/BasicGrpc)
  • Stage2 - 데이터베이스와 인증을 포함한 전체 웹 애플리케이션입니다(/src/BenchmarksApps/TodosApi)

.NET 배포 시나리오

테스트 애플리케이션은 서로 다른 환경에서 실행됩니다. 현재 벤치마크는 28개 코어를 가진 Windows 및 Linux 가상 머신을 사용합니다. ARM 및 Intel 프로세서용 별도 Linux 환경도 있습니다.

애플리케이션은 서로 다른 구성으로도 테스트됩니다. 어떤 구성의 애플리케이션이 하나의 "시나리오"를 정의합니다.

PowerBI 대시보드에서 Ctrl(또는 ⌘) 키를 누른 채 여러 시나리오나 환경을 선택할 수 있습니다.

메트릭

벤치마크는 배포된 모든 애플리케이션에 대해 기본 메트릭을 수집합니다. 예를 들어 초당 요청 수(RPS), 시작 시간, 최대 메모리 워킹 세트를 측정합니다.

이를 통해 동일한 애플리케이션의 다양한 구성에 대한 메트릭 값을 비교할 수 있습니다.

성능 비교

StageX 시나리오를 StageXAot 및 StageXAotSpeedOpt와 비교합니다. 이들은 다음 구성을 사용합니다:

Scenario dotnet publish build arguments
StageX PublishAot=false
EnableRequestDelegateGenerator=false
Stage2Aot PublishAot=true
StripSymbols=true
Stage2AotSpeedOpt PublishAot=true
StripSymbols=true
OptimizationPreference=Speed

위의 모든 시나리오는 DOTNET_GCDynamicAdaptationMode=1 환경 변수를 함께 사용합니다.

StageXAotSpeedOpt 시나리오는 OptimizationPreference = Speed 설정의 영향을 추정하는 데 도움이 됩니다.

StageXTrimR2RSingleFile 시나리오도 검토할 수 있습니다. 이러한 시나리오는 .NET에서 또 다른 형태의 사전 컴파일인 trimmed ReadyToRun 배포에 해당합니다. 경우에 따라 Native AOT의 좋은 대안이 됩니다.

다음은 2024년 9월의 .NET 9 Release Candidate에 대한 현재 성능 비교 결과입니다.

시작 시간

AOT 애플리케이션은 관리형 버전보다 훨씬 더 빠르게 시작합니다. 이는 Stage1과 Stage2 애플리케이션 모두, 그리고 모든 환경에서 마찬가지입니다. 샘플 결과는 다음과 같습니다.

Scenario Startup time (ms)
Stage2AotSpeedOpt 100
Stage2Aot 109
Stage2 528

워킹 세트

Native AOT 애플리케이션의 최대 워킹 세트는 관리형 버전보다 작습니다. Linux에서는 관리형 버전이 AOT 버전보다 대략 1.5~2배 더 많은 RAM을 사용합니다. 예를 들면:

Scenario Max working set (MB)
Stage1Aot 56
Stage1AotSpeedOpt 57
Stage1 126

Windows에서는 차이가 더 작습니다. 특히 Stage2에서 그렇습니다.

Scenario Max working set (MB)
Stage2Aot 152
Stage2AotSpeedOpt 150
Stage2 167

초당 요청 수

RPS 값이 클수록 애플리케이션이 더 빠릅니다. 가벼운 Stage1 애플리케이션은 일반적으로 초당 약 80만~90만 요청을 처리합니다. 더 큰 Stage2 애플리케이션은 약 20만 요청만 처리합니다.

Stage2 애플리케이션의 경우, .NET 버전이 모든 환경에서 AOT 버전보다 더 많은 요청을 처리합니다. Stage2AotSpeedOpt 버전의 속도는 때때로 비슷합니다. 하지만 보통은 Stage2와 Stage2Aot 사이에 있습니다. 일반적인 결과는 다음과 같습니다.

Scenario RPS
Stage2 235,008
Stage2AotSpeedOpt 215,637
Stage2Aot 194,264

Stage1 애플리케이션의 결과는 Intel Linux와 Intel Windows에서 비슷합니다. 그러나 Ampere Linux에서는 AOT가 관리형 버전을 능가합니다. Ampere Linux의 샘플 결과는 다음과 같습니다.

Scenario RPS
Stage1AotSpeedOpt 929,524
Stage1Aot 912,344
Stage1 844,659

따라서 환경과 애플리케이션 코드는 속도에 큰 영향을 줄 수 있습니다. 프로젝트에서 Native AOT의 이점을 추정하려면 자체 벤치마크를 실행하는 것이 타당합니다. 이제 Microsoft 테스트 인프라 없이 사용자 지정 벤치마크를 작성해 보겠습니다.

Native AOT 애플리케이션 벤치마킹

두 가지 유형의 벤치마크를 사용합니다. 첫 번째는 .NET 코드 벤치마킹에 널리 쓰이는 라이브러리인 BenchmarkDotNet 기반입니다. 이 벤치마크는 시작 시간을 제외한 순수 속도를 비교합니다.

두 번째는 hyperfine 도구 기반입니다. 이 도구는 두 셸 명령의 실행 시간을 비교할 수 있습니다. 이 벤치마크는 시작 시간을 포함한 전체 속도를 비교합니다.

여기서는 메모리 사용량을 비교하지 않습니다. 현재 BenchmarkDotNet의 NativeMemoryProfiler diagnoser는 Native AOT 런타임을 지원하지 않습니다. hyperfine도 현재 메모리 사용량을 추적하지 않습니다.

소스 코드는 GitHub의 NativeAotBenchmarks 저장소에서 다운로드할 수 있습니다. 자신의 환경에서 직접 실행해 보시기 바랍니다. 이 문서는 Intel Core i9-13900H 프로세서와 16 Gb RAM을 갖춘 Windows 11 노트북에서 얻은 결과를 설명합니다.

벤치마크를 올바르게 실행해야 합니다. 일반적인 권장 사항은 다음과 같습니다.

  • Release 빌드를 사용합니다.
  • 벤치마크 프로세스 외의 모든 애플리케이션을 종료합니다. 예를 들어 바이러스 백신을 비활성화하고, Visual Studio와 웹 브라우저를 닫습니다.
  • 노트북은 전원에 연결하고 최고의 성능 모드를 사용합니다.
  • 비교하는 시나리오에서 같은 입력 데이터를 사용합니다.

테스트 케이스

.NET 8에서 2가지 시나리오를 벤치마크합니다.

1. 반복 문자 개수를 사용하는 문자열 압축을 위한 간단한 C# 코드입니다. 예를 들어 문자열 "aabcccccaaa"는 "a2b1c5a3"가 됩니다.

string Compress(string s)
{
    StringBuilder compressed = new(s.Length);

    for (int i = 0; i < s.Length; ++i)
    {
        char c = s[i];
        for (int j = i + 1; j <= s.Length; ++j)
        {
            if (j == s.Length || s[j] != c)
            {
                compressed.Append(c + $"{j - i}");
                i = j - 1;

                if (compressed.Length > s.Length)
                    return s;

                break;
            }
        }
    }

    if (compressed.Length <= s.Length)
        return compressed.ToString();

    return s;
}

2. Docotic.Pdf를 사용하는 더 무거운 PDF를 PNG로 변환 작업입니다.

필수 조건

.NET Native AOT 배포를 위한 필수 구성 요소를 설치합니다.

해당 벤치마크를 실행하려면 hyperfine 설치를 하십시오.

PDF를 PNG로 벤치마크하려면 C# .NET PDF 라이브러리 다운로드 페이지에서 무료 기간 제한 라이선스 키를 받으십시오. 라이선스 키는 Helper.cs에 적용해야 합니다.

BenchmarkDotNet

이 벤치마크는 NativeAotBenchmarks 프로젝트에 있습니다. RuntimeMoniker.NativeAot80과 RuntimeMoniker.Net80의 결과를 비교합니다. 기본적으로 BenchmarkDotNet은 OptimizationPreference=Speed 설정으로 Native AOT 코드를 빌드합니다.

BenchmarkDotNet은 6회 이상 워밍업 반복을 수행합니다. 이는 JIT가 코드를 미리 컴파일하고 일부 통계를 수집하는 데 도움이 됩니다. 따라서 이러한 benhmarks는 시작 시간을 비교에서 제외합니다.

문자열 압축

문자열 압축을 위한 CompressString 벤치마크는 중복 문자가 있는 긴 문자열을 사용합니다. 흔한 실수는 랜덤 문자열을 생성하는 것입니다. 그런 경우 Native AOT와 .NET 8 벤치마크는 서로 다른 입력 문자열을 사용하게 됩니다. 랜덤 문자열을 사용할 수는 있지만, 같은 시드로 랜덤 생성기를 초기화해야 합니다.

Native AOT 버전은 .NET 8 버전보다 약 1.08배 더 빠르게 실행됩니다.

Method Runtime Mean Error StdDev
Compress .NET 8.0 4.117 ms 0.0553 ms 0.0517 ms
Compress NativeAOT 8.0 3.809 ms 0.0403 ms 0.0377 ms

PDF를 PNG로

PDF를 PNG로 벤치마크는 PDF 문서를 메모리에서 처리합니다. 이를 통해 파일 시스템과의 상호작용을 제외할 수 있습니다. 디스크 I/O 작업은 벤치마크 결과를 왜곡할 수 있습니다.

두 개의 PDF 문서를 사용해 속도를 테스트합니다. 첫 번째 문서인 Banner Edulink One.pdf는 더 복잡합니다. 이 문서는 72 dpi PNG로 변환되며 처리에 더 많은 시간이 필요합니다. 이 문서에서는 .NET 8 버전이 약간 더 빠릅니다.

Method Runtime Mean Error StdDev
Convert .NET 8.0 1.103 s 0.0156 s 0.0146 s
Convert NativeAOT 8.0 1.167 s 0.0160 s 0.0149 s

두 번째 문서는 더 작고 단순합니다. 300 dpi PNG로 변환되며 속도는 거의 같습니다.

Method Runtime Mean Error StdDev
Convert .NET 8.0 290.1 ms 5.78 ms 6.88 ms
Convert NativeAOT 8.0 288.3 ms 4.44 ms 3.94 ms

hyperfine

이 벤치마크는 NativeAotTestApp 프로젝트에 있습니다. 이 프로젝트는 OptimizationPreference=Speed 설정을 사용하지 않습니다. NativeAotTestApp.csproj에서 다음을 추가해 활성화할 수 있습니다: <OptimizationPreference>Speed</OptimizationPreference>

Windows에서 테스트를 실행하려면 benchmark.bat 스크립트를 사용합니다. Unix/Linux 기반 운영 체제용으로 Bash로 변환할 수 있습니다. 이 스크립트는 동일한 앱의 .NET 8 버전과 Native AOT 버전을 빌드합니다. سپس 비슷한 명령으로 성능을 비교합니다: hyperfine --warmup 3 "net8-app.exe" "native-aot-app.exe"

hyperfine의 워밍업 실행은 테스트 애플리케이션을 "따뜻한" 디스크 캐시에서 시작하는 데 도움이 됩니다. BenchmarkDotNet과 달리 hyperfine의 워밍업은 JIT에 도움이 되지 않습니다. 따라서 hyperfine 벤치마크는 시작 시간을 포함한 전체 애플리케이션 속도를 비교합니다.

테스트 애플리케이션은 반복 횟수 인수를 지원합니다. 이를 통해 간단한 루프에서 동일한 코드를 여러 번 반복할 수 있습니다.

for (int i = 0; i < iterationCount; ++i)
    CompressString(args);

아이디어는 시작 시간 차이의 영향을 줄이는 것입니다. 동일한 코드를 반복하면 JIT가 더 많은 런타임 통계를 수집하고 더 빠른 코드를 생성할 기회를 얻습니다.

일반적인 상황은 다음과 같습니다. 처음에는 단일 반복으로 벤치마크를 실행합니다. Native AOT 버전이 훨씬 더 빠르게 동작합니다. 그런 다음 동일한 벤치마크를 여러 반복으로 실행하면 두 버전의 총 속도가 같아집니다. 이는 시작 후에는 관리형 버전이 실제로 더 빠르다는 뜻입니다.

문자열 압축

동일한 입력 문자열 압축을 100,000회 반복하면 Native AOT 성능이 더 좋습니다.

Benchmark 1: .NET 8 version (100000 iterations)
  Time (mean ± σ):     151.5 ms ±   2.6 ms    [User: 32.1 ms, System: 1.6 ms]
  Range (min … max):   148.0 ms … 157.5 ms    19 runs

Benchmark 2: Native AOT version (100000 iterations)
  Time (mean ± σ):      55.1 ms ±   3.1 ms    [User: 15.0 ms, System: 2.1 ms]
  Range (min … max):    51.6 ms …  65.9 ms    51 runs

Summary
  Native AOT version ran 2.75 ± 0.16 times faster than .NET 8 version

하지만 10,000,000회 반복에서는 속도가 거의 같습니다.

Benchmark 1: .NET 8 version (10000000 iterations)
  Time (mean ± σ):      3.984 s ±  0.139 s    [User: 2.946 s, System: 0.009 s]
  Range (min … max):    3.790 s …  4.182 s    10 runs

Benchmark 2: Native AOT version (10000000 iterations)
  Time (mean ± σ):      3.956 s ±  0.041 s    [User: 2.848 s, System: 0.004 s]
  Range (min … max):    3.888 s …  4.016 s    10 runs

Summary
  Native AOT version ran 1.01 ± 0.04 times faster than .NET 8 version

PDF를 PNG로

Banner Edulink One.pdf를 PNG로 변환하는 단일 반복에서는 AOT 버전이 .NET 8 버전보다 약 1.88배 더 빠르게 실행됩니다.

Benchmark 1: .NET 8 version (1 iteration)
  Time (mean ± σ):      2.417 s ±  0.104 s    [User: 1.334 s, System: 0.116 s]
  Range (min … max):    2.295 s …  2.629 s    10 runs

Benchmark 2: Native AOT version (1 iteration)
  Time (mean ± σ):      1.288 s ±  0.011 s    [User: 0.573 s, System: 0.123 s]
  Range (min … max):    1.274 s …  1.310 s    10 runs

20회 반복에서는 속도 차이가 무시할 수 있을 정도입니다.

Benchmark 1: .NET 8 version (20 iterations)
  Time (mean ± σ):     25.048 s ±  0.223 s    [User: 13.278 s, System: 2.312 s]
  Range (min … max):   24.751 s … 25.423 s    10 runs

Benchmark 2: Native AOT version (20 iterations)
  Time (mean ± σ):     25.213 s ±  0.114 s    [User: 12.661 s, System: 2.275 s]
  Range (min … max):   25.042 s … 25.350 s    10 runs

Summary
  .NET 8 version ran 1.01 ± 0.01 times faster than Native AOT version

3BigPreview.pdf의 경우, Native AOT 버전은 100회 반복에서도 더 빠릅니다.

Benchmark 1: .NET 8 version (100 iterations)
  Time (mean ± σ):     10.009 s ±  0.152 s    [User: 5.298 s, System: 0.567 s]
  Range (min … max):    9.677 s … 10.189 s    10 runs

Benchmark 2: Native AOT version (100 iterations)
  Time (mean ± σ):      8.336 s ±  0.070 s    [User: 3.405 s, System: 0.505 s]
  Range (min … max):    8.247 s …  8.459 s    10 runs

Summary
  Native AOT version ran 1.20 ± 0.02 times faster than .NET 8 version

결론

Native AOT 애플리케이션은 일반적인 .NET보다 더 빠르게 시작합니다. 공식 벤치마크도 AOT 애플리케이션의 메모리 사용량이 더 작다는 것을 보여줍니다.

하지만 시작 후에는 관리형 애플리케이션이 일반적으로 더 나은 속도를 보입니다. 이는 JIT가 런타임 정보에 접근할 수 있기 때문입니다. 장시간 실행되는 애플리케이션에서는 동적 프로파일 기반 최적화와 기타 기법을 바탕으로 더 효과적인 코드를 다시 생성할 수 있습니다.

ASP.NET 벤치마크를 사용하면 성능 관점에서 서로 다른 구성을 비교할 수 있습니다. 그러나 결과는 운영 체제와 프로세서 아키텍처에 따라 달라집니다. 프로젝트에 최적의 배포 구성을 찾으려면 대상 환경에서 자체 벤치마크를 실행해야 합니다.