.NET 컴파일 방식 비교: AOT vs JIT
Post

.NET 컴파일 방식 비교: AOT vs JIT

.NET에서는 애플리케이션을 실행 가능한 코드로 변환하는 방식으로 JIT(Just-In-Time)과 AOT(Ahead-Of-Time) 두 가지를 지원합니다. 이 글에서는 두 방식을 비교하고 각각의 특징과 차이점을 정리합니다.

주요 내용은 다음과 같습니다.

  • JIT(Just-In-Time) 컴파일 방식
  • JIT 컴파일러의 종류
  • AOT(Ahead-Of-Time) 컴파일 방식
  • AOT와 JIT 비교
  • Native AOT와 ReadyToRun(R2R) 비교

JIT(Just-In-Time) 컴파일 방식

JIT 컴파일은 .NET에서 기본적으로 사용하는 컴파일 방식입니다. 일반적으로 .NET 애플리케이션을 빌드하면 IL(Intermediate Language) 코드가 포함된 .dll 또는 .exe 파일이 생성됩니다. 이 파일은 직접 실행되는 것이 아니라, CLR(Common Language Runtime)이라는 실행 환경이 읽어들여 실행합니다.

실행 시에는 다음과 같은 흐름을 따릅니다.

  1. 프로그램이 실행되면 CLR이 IL 코드를 읽습니다.
  2. 메서드가 호출되는 순간, JIT 컴파일러가 해당 메서드를 네이티브 코드(머신 코드)로 변환합니다.
  3. 변환된 네이티브 코드는 메모리에 캐시됩니다.
  4. 동일한 메서드가 다시 호출될 때는 이미 캐시된 네이티브 코드를 바로 실행합니다.

이 때문에 JIT 방식은 첫 호출 시에는 약간의 지연이 발생하지만, 이후 호출에서는 빠른 속도로 실행됩니다.

  • 특징
    • CLR이 런타임에 필요한 시점에만 메서드를 컴파일합니다.
    • 초기 실행 속도가 느릴 수 있습니다.
    • 실행 중 프로파일링 정보를 활용한 추가 최적화가 가능합니다.
    • 컴파일된 네이티브 코드는 메모리에 캐시되어 재사용됩니다.
  • 장점
    • 다양한 플랫폼(OS, CPU 아키텍처)에서 동일한 IL 바이너리를 재사용할 수 있습니다.
    • 실행 환경에 최적화된 코드를 생성할 수 있습니다.
    • 빌드 속도가 빠릅니다.
  • 단점
    • 메서드 호출 시 초기 컴파일 지연이 발생할 수 있습니다.
    • 런타임 성능이 일관되지 않을 수 있습니다.
    • 메모리 사용량이 증가할 수 있습니다.

JIT 컴파일러의 종류

JIT 방식은 하나의 컴파일 방식이지만, 이를 실제로 구현한 컴파일러는 플랫폼과 런타임 환경에 따라 다양합니다. 대표적인 JIT 컴파일러는 다음과 같습니다.

JIT 컴파일러설명
RyuJIT.NET Core 및 .NET 5 이상에서 기본으로 사용하는 JIT 컴파일러입니다. x64 및 ARM64 아키텍처를 지원하며, 지속적으로 최적화되고 있습니다.
Legacy JIT.NET Framework (특히 32비트 환경)에서 사용되던 이전 세대 JIT 컴파일러입니다. 현재는 유지보수가 중단되었습니다.
Mono JITXamarin, Unity, 일부 Linux 환경 등 Mono 런타임 기반에서 사용되는 JIT입니다. 플랫폼에 따라 커스터마이징되어 있으며, 다양한 운영체제를 지원합니다.

이러한 JIT 컴파일러는 모두 공통적으로 IL 코드를 실행 시점에 네이티브 코드로 변환하는 방식을 따르지만, 성능, 플랫폼 지원, 내부 최적화 기술 등에서 차이를 가집니다.

AOT(Ahead-Of-Time) 컴파일 방식

AOT 컴파일은 빌드 타임에 IL 코드를 미리 네이티브 코드로 변환하는 방식입니다. AOT 빌드를 수행하면 생성된 .exe 또는 .dll 파일 안에는 IL이 아니라 완성된 네이티브 코드가 포함됩니다.

AOT 실행 흐름은 다음과 같습니다.

  1. 빌드 타임에 모든 IL 코드를 네이티브 코드로 변환합니다.
  2. 실행 시에는 변환 없이 네이티브 코드를 바로 OS 위에서 실행합니다.
  3. CLR을 통한 JIT 컴파일 과정이 필요하지 않습니다.
  • 특징
    • 모든 코드를 사전에 컴파일합니다.
    • 실행 파일 크기가 커질 수 있습니다.
    • 실행 시간이 매우 빠릅니다.
  • 장점
    • 애플리케이션 시작 속도가 빠릅니다.
    • 외부에 CLR을 설치하지 않고 실행할 수 있습니다.
    • 메모리 사용량이 예측 가능합니다.
    • 리버스 엔지니어링이 상대적으로 어렵습니다.
  • 단점
    • 빌드 시간이 길어질 수 있습니다.
    • 리플렉션(reflection)이나 런타임 코드 생성(dynamic code generation)에 제약이 있을 수 있습니다.
    • 빌드 시점에 대부분의 최적화를 완료해야 합니다.

AOT vs JIT 비교

항목JITAOT
컴파일 시점런타임빌드 타임
빌드 산출물IL 코드(.dll, .exe)네이티브 코드(.exe, .dll)
실행 속도초기 느림, 이후 최적화매우 빠름
빌드 시간빠름느림
바이너리 크기작음
플랫폼 독립성높음 (IL 공유)낮음 (플랫폼별 빌드 필요)
런타임 최적화가능제한적

Native AOT와 ReadyToRun(R2R) 비교

.NET에서는 실행 성능을 개선하기 위한 AOT 컴파일 방식을 여러 형태로 지원합니다. 그 중 대표적인 것이 Native AOTReadyToRun(R2R)입니다.

항목Native AOTReadyToRun (R2R)
목적완전한 네이티브 실행 파일 생성JIT 부하를 줄여 초기 실행 속도 개선
빌드 결과물네이티브 코드만 포함IL + 일부 미리 컴파일된 코드 포함
시스템에 .NET CLR 필요 여부필요 없음필요함
파일 크기상대적으로 작음상대적으로 큼
런타임 최적화제한적일부 가능
리플렉션, 동적 로딩 지원제한됨지원
  • Native AOT
    • 애플리케이션을 완전히 네이티브 바이너리로 컴파일합니다.
    • .NET CLR이 시스템에 없어도 실행할 수 있습니다.
    • 대신, 리플렉션과 런타임 코드 생성 기능에 제약이 있습니다.
  • ReadyToRun (R2R)
    • 주요 코드 경로를 미리 컴파일하여 초기 실행 속도를 개선합니다.
    • 여전히 IL 코드와 함께 빌드되어 .NET CLR이 필요합니다.
    • 런타임 최적화 및 리플렉션 사용이 가능합니다.

정리

.NET은 다양한 실행 환경과 성능 요구를 지원하기 위해 JIT과 AOT 두 가지 컴파일 방식을 제공합니다. JIT 방식은 빌드 속도가 빠르고 다양한 플랫폼에 유연하게 대응할 수 있지만, 실행 초기에 지연이 발생할 수 있습니다. 반면 AOT 방식은 빌드 시간은 길어지지만, 빠른 시작과 일관된 성능을 제공합니다.

애플리케이션의 특성에 따라 다음과 같이 선택할 수 있습니다.

  • 빠른 시작과 배포 파일 일체화를 원할 경우 AOT 방식이 적합합니다.
  • 다양한 플랫폼 호환성과 동적 기능을 중시할 경우 JIT 방식이 유리합니다.

또한 AOT 방식 내에서도 Native AOT와 ReadyToRun을 구분하여, CLR 의존성과 배포 방식에 맞게 적절히 선택하는 것이 중요합니다.

Reference

.NET 플랫폼 정리: 플랫폼, Standard, 구현체, 그리고 변화

Python 패키지 관리의 진화: Poetry 도입 배경과 사용법