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

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 환경도 있습니다.

애플리케이션은 또한 다양한 구성에서 테스트됩니다. 일부 구성의 애플리케이션은 "시나리오"를 정의합니다.

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

메트릭

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

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

성능 비교

StageX 시나리오를 StageXAot 및 StageXAotSpeedOpt와 비교하겠습니다. 다음 구성을 사용합니다.

시나리오 dotnet publish 빌드 인수
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에서 사전 컴파일의 또 다른 형태인 트리밍된 ReadyToRun 배포에 해당합니다. 때때로 Native AOT에 대한 좋은 대안이 됩니다.

다음은 .NET 9 릴리스 후보(2024년 9월)의 현재 성능 비교 결과입니다.

시작 시간

AOT 애플리케이션은 관리형 버전보다 훨씬 빠르게 시작됩니다. 이는 Stage1 및 Stage2 애플리케이션과 모든 환경에 해당합니다. 샘플 결과:

시나리오 시작 시간(ms)
Stage2AotSpeedOpt 100
Stage2Aot 109
Stage2 528

작업 세트

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

시나리오 최대 작업 세트(MB)
Stage1Aot 56
Stage1AotSpeedOpt 57
Stage1 126

Windows에서는 차이가 더 작습니다. 특히 Stage2의 경우:

시나리오 최대 작업 세트(MB)
Stage2Aot 152
Stage2AotSpeedOpt 150
Stage2 167

초당 요청 수

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

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

시나리오 RPS
Stage2 235,008
Stage2AotSpeedOpt 215,637
Stage2Aot 194,264

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

시나리오 RPS
Stage1AotSpeedOpt 929,524
Stage1Aot 912,344
Stage1 844,659

따라서 환경과 애플리케이션 코드는 속도에 상당한 영향을 미칠 수 있습니다. 프로젝트에 대한 Native AOT 이점을 추정하기 위해 자체 벤치마크를 실행하는 것이 합리적입니다. Microsoft 테스트 인프라 없이 사용자 지정 벤치마크를 작성해 보겠습니다.

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

우리는 2가지 유형의 벤치마크를 사용할 것입니다. 첫 번째는 .NET 코드 벤치마킹을 위한 인기 있는 라이브러리인 BenchmarkDotNet을 기반으로 합니다. 이러한 벤치마크는 시작 시간을 제외한 순수한 속도를 비교합니다.

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

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

GitHub의 NativeAotBenchmarks 저장소에서 소스 코드를 다운로드할 수 있습니다. 사용자 환경에서 시도해 보는 것이 좋습니다. 이 문서에서는 Intel Core i9-13900H 프로세서와 16Gb RAM이 있는 Windows 11 노트북의 결과를 설명합니다.

벤치마크를 제대로 실행했는지 확인하세요. 일반적인 권장 사항은 다음과 같습니다.

  • 릴리스 빌드를 사용합니다.
  • 벤치마크 프로세스를 제외한 모든 애플리케이션을 끕니다. 예를 들어, 바이러스 백신 소프트웨어를 비활성화하고 Visual Studio와 웹 브라우저를 닫습니다.
  • 노트북을 플러그에 꽂아두고 최상의 성능 모드를 사용합니다.
  • 비교되는 시나리오에서 동일한 입력 데이터를 사용합니다.

테스트 사례

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

1. 반복되는 문자의 개수를 사용하여 문자열을 압축하는 간단한 C# 코드. 예를 들어, 문자열 "aabccccaaa"는 "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가 코드를 사전 컴파일하고 일부 통계를 수집하는 데 도움이 됩니다. 따라서 이러한 벤치마크는 시작 시간을 비교에서 제외합니다.

문자열 압축

문자열 압축을 위한 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 벤치마크를 사용하면 성능 관점에서 다양한 구성을 비교할 수 있습니다. 그러나 결과는 운영 체제와 프로세서 아키텍처에 따라 달라집니다. 최적의 배포 구성을 찾으려면 대상 환경에서 자체 벤치마크를 실행해야 합니다.