JIT Compiler vs. AOT Compiler

JIT 컴파일러와 AOT 컴파일러는 모두 소스 코드 또는 중간 표현(바이트코드)을 기계어 코드로 변환한다는 공통점을 가지지만, 언제 그리고 어떻게 컴파일하는지에 큰 차이가 있다.

JIT와 AOT 컴파일러는 각각 고유한 장단점을 가지고 있으며, 사용 환경과 요구사항에 따라 적합한 접근 방식이 달라진다.
JIT 컴파일러는 런타임 정보를 활용한 최적화와 플랫폼 독립성을 제공하는 반면, AOT 컴파일러는 빠른 시작 시간과 예측 가능한 성능을 제공한다.

현대 소프트웨어 개발에서는 이 두 접근 방식의 장점을 결합한 하이브리드 방식이 점점 더 인기를 얻고 있다.
앞으로는 기계 학습, 특화된 하드웨어 활용, WebAssembly 확산 등의 동향이 컴파일러 기술의 발전을 이끌 것으로 예상된다.

JIT 컴파일러 (Just-In-Time Compiler)

JIT 컴파일러는 프로그램 실행 중에 필요한 부분을 동적으로 컴파일하는 방식이다. 이는 “필요한 시점에(Just-In-Time)” 컴파일이 이루어진다는 의미이다.

JIT 컴파일러의 작동 원리

프로그램이 처음 실행될 때는 인터프리터 방식으로 동작하다가, 반복적으로 실행되는 “핫스팟” 코드 영역을 감지하면 해당 부분을 네이티브 머신 코드로 변환한다. 이때 런타임 프로파일링 정보를 기반으로 최적화(예를 들어 함수 인라인, 불필요한 코드 제거 등)를 수행할 수 있다.

JIT 컴파일의 기본 과정은 다음과 같다:

  1. 중간 코드 생성: 소스 코드는 먼저 바이트코드나 중간 표현(IR)으로 변환된다.
  2. 인터프리터 실행: 처음에는 이 중간 코드가 인터프리터에 의해 한 명령어씩 실행된다.
  3. 프로파일링: 런타임 시스템이 코드 실행을 모니터링하면서 자주 실행되는 코드 영역(핫스팟)을 식별한다.
  4. 동적 컴파일: 식별된 핫스팟 코드는 네이티브 기계어로 컴파일된다.
  5. 최적화 적용: 런타임 정보(타입 정보, 실행 패턴 등)를 활용하여 다양한 최적화를 적용한다.
  6. 코드 캐싱: 컴파일된 네이티브 코드는 코드 캐시에 저장되어 재사용된다.

JIT 컴파일러의 장단점

장점
  1. 런타임 정보 활용: 실제 실행 패턴에 기반한 최적화가 가능하다.
  2. 플랫폼 독립성: 중간 코드는 플랫폼 독립적이어서 “한 번 작성하고 어디서나 실행” 패러다임이 가능하다.
  3. 동적 기능 지원: 리플렉션, 동적 클래스 로딩 등의 동적 기능을 완전히 지원한다.
  4. 적응형 최적화: 애플리케이션의 실행 패턴에 따라 최적화 전략을 조정할 수 있다.
  5. 작은 배포 크기: 중간 코드는 네이티브 코드보다 일반적으로 크기가 작다.
단점
  1. 시작 지연: 초기 컴파일로 인해 프로그램 시작이 느릴 수 있다.
  2. 메모리 사용량 증가: JIT 컴파일러 자체, 프로파일링 데이터, 코드 캐시 등으로 메모리 사용량이 증가한다.
  3. 예측 불가능한 성능: 컴파일 타이밍에 따라 성능이 변동될 수 있다.
  4. 배터리 소모: 모바일 환경에서 컴파일 작업은 추가 전력을 소모한다.

JIT 컴파일러 구현 사례

  1. Java의 HotSpot JVM: 클라이언트 컴파일러(C1)와 서버 컴파일러(C2)를 조합한 계층적 컴파일 방식을 사용한다.
  2. V8 JavaScript 엔진: Ignition 인터프리터와 TurboFan 최적화 컴파일러를 사용한다.
  3. PyPy: 메타 추적 JIT를 사용하여 Python 코드를 최적화한다.
  4. .NET의 RyuJIT: C#, F#, VB.NET 등을 위한 JIT 컴파일러이다.

AOT 컴파일러 (Ahead-Of-Time Compiler)

AOT 컴파일러는 프로그램 실행 전에, 즉 빌드 타임에 전체 코드를 미리 네이티브 머신 코드로 번역한다.

AOT 컴파일러의 작동 원리

소스 코드 또는 바이트코드를 전체적으로 분석하고, 정적 최적화를 수행한 후 대상 플랫폼에 최적화된 기계어 코드로 변환한다. 런타임에 추가적인 컴파일 과정을 거치지 않으므로 즉시 실행이 가능하며, 예측 가능한 성능을 제공한다.

AOT 컴파일의 기본 과정은 다음과 같다:

  1. 소스 코드 분석: 소스 코드를 파싱하고 구문 분석한다.
  2. 중간 표현 생성: 언어 중립적인 중간 표현(IR)으로 코드를 변환한다.
  3. 정적 최적화: 다양한 최적화 패스를 통해 코드를 개선한다.
  4. 타겟 코드 생성: 특정 대상 아키텍처의 네이티브 기계어로 코드를 변환한다.
  5. 링킹: 생성된 목적 파일을 라이브러리 및 다른 목적 파일과 연결하여 최종 실행 파일을 생성한다.

장단점

장점
  1. 빠른 시작 시간: 실행 파일이 이미 네이티브 코드로 컴파일되어 있어 시작이 빠르다.
  2. 예측 가능한 성능: 컴파일 시간에 최적화가 완료되어 일관된 성능을 제공한다.
  3. 낮은 메모리 사용량: 런타임에 컴파일러가 필요 없어 메모리 사용량이 감소한다.
  4. 향상된 보안: 소스 코드나 중간 표현이 배포되지 않아 역공학이 더 어렵다.
  5. 배터리 수명 향상: 컴파일 작업이 없어 CPU 사용량이 감소하며, 이는 모바일 기기의 배터리 수명 연장으로 이어진다.
단점
  1. 증가된 파일 크기: 네이티브 코드는 중간 표현보다 일반적으로 크기가 크다.
  2. 플랫폼 의존성: 각 목표 플랫폼/아키텍처별로 별도 컴파일이 필요하다.
  3. 동적 최적화 부재: 런타임 정보를 활용한 최적화가 제한적이다.
  4. 컴파일 시간 증가: 개발-빌드-테스트 주기에 시간이 추가된다.
  5. 리플렉션 및 동적 기능 제한: 리플렉션, 동적 클래스 로딩과 같은 동적 기능이 제한된다.

AOT 컴파일러 구현 사례

  1. GCC/Clang: C, C++, Objective-C 등을 위한 표준 AOT 컴파일러.
  2. Rust 컴파일러(rustc): Rust 언어를 위한 LLVM 기반 컴파일러.
  3. Go 컴파일러: Go 언어를 위한 AOT 컴파일러.
  4. Swift 컴파일러: Swift 코드를 네이티브 코드로 컴파일한다.
  5. NativeAOT(.NET):.NET 애플리케이션을 네이티브 코드로 컴파일한다.
  6. Graal Native Image: Java 애플리케이션을 독립 실행형 네이티브 실행 파일로 컴파일한다.

하이브리드 접근법: AOT와 JIT의 결합

최근에는 AOT와 JIT의 장점을 결합한 하이브리드 접근법이 인기를 얻고 있다:

  1. Android Runtime (ART)
    Android 7.0(Nougat)부터는 AOT, JIT, 프로필 기반 컴파일을 혼합하여 사용한다:

    • 앱 설치 시: 일부 AOT 컴파일
    • 첫 실행 시: JIT 컴파일 + 사용 패턴 프로파일링
    • 기기 유휴 상태 + 충전 중: 프로파일 기반으로 자주 사용되는 코드 AOT 컴파일
  2. .NET Native/CoreRT
    .NET 애플리케이션을 네이티브 코드로 컴파일하지만, 리플렉션과 같은 동적 기능을 지원하기 위해 제한된 JIT 컴파일을 포함한다.

  3. GraalVM Native Image
    Java 애플리케이션을 네이티브 이미지로 컴파일하지만, 동적 기능을 위한 런타임 지원을 포함한다.

실제 사용 사례 분석

  1. 모바일 애플리케이션
    모바일 환경에서는 시작 시간, 메모리 사용량, 배터리 수명이 중요하다:

    • iOS: Swift와 Objective-C는 AOT 컴파일을 사용하여 빠른 시작과 효율적인 전력 사용을 제공한다.
    • Android: 하이브리드 접근법(ART)을 사용하여 시작 시간과 장기 실행 성능의 균형을 맞춘다.
    • Flutter: AOT 컴파일(릴리스 모드)과 JIT 컴파일(개발 모드)을 모두 지원한다.
  2. 웹 애플리케이션
    웹 브라우저에서 JavaScript 실행은 JIT 컴파일에 크게 의존한다:

    • V8(Chrome): 여러 단계의 JIT 컴파일을 사용하여 JavaScript 성능을 최적화한다.
    • WebAssembly: C/C++, Rust 등으로 작성된 코드를 AOT 컴파일하여 웹에서 네이티브에 가까운 성능을 제공한다.
  3. 서버 애플리케이션
    서버 환경에서는 장기 실행 성능이 중요하다:

    • Java: 전통적으로 JIT 컴파일을 사용하지만, GraalVM Native Image와 같은 AOT 옵션도 있다.
    • Go: AOT 컴파일을 사용하여 빠른 시작과 예측 가능한 성능을 제공한다.
    • Node.js: V8 JIT를 사용하여 JavaScript 서버 코드를 최적화한다.
  4. 임베디드 시스템
    제한된 리소스를 가진 임베디드 환경에서는 AOT 컴파일이 선호된다:

    • C/C++: 메모리와 성능 제약이 있는 임베디드 시스템에서 AOT 컴파일이 표준이다.
    • MicroPython: 제한된 리소스를 위해 인터프리터와 AOT 컴파일의 혼합을 사용한다.

JIT와 AOT 컴파일러 비교 분석

특성JIT 컴파일러 (Just-In-Time)AOT 컴파일러 (Ahead-Of-Time)
컴파일 시점프로그램 실행 중에 컴파일프로그램 실행 전에 컴파일
동작 방식초기엔 인터프리터 방식 → 핫스팟 코드를 감지하여 동적 컴파일전체 코드를 미리 분석, 정적 최적화 후 기계어로 변환
코드 변환 과정바이트코드/중간 코드 → 프로파일링 → 필요한 부분만 네이티브 코드로 컴파일소스 코드/중간 코드 → 전체 코드를 네이티브 코드로 컴파일 → 실행
성능초기 실행은 느리지만, 핫스팟 컴파일 후 반복 실행 시 높은 성능 제공프로그램 시작 즉시 최적화된 코드 실행, 예측 가능한 성능 제공
시작 시간느림 (첫 실행 시 컴파일 오버헤드)빠름 (이미 컴파일되어 있음)
장기 실행 성능매우 높음 (런타임 프로파일링 기반 최적화)높음 (정적 최적화)
메모리 사용량높음 (컴파일러, 프로파일러, 코드 캐시 필요)낮음 (런타임에 컴파일러 불필요)
최적화 수준실행 패턴 기반 적응형 최적화 가능정적 분석 기반 최적화, 일부 PGO(Profile-Guided Optimization) 가능
최적화런타임 프로파일링 기반의 동적 최적화 (함수 인라인, 불필요한 코드 제거 등)빌드 타임에 전 범위 최적화를 적용하지만 런타임 정보 반영은 어려움
코드 최적화 범위핫스팟 위주 (자주 실행되는 코드)전체 코드 (균일한 최적화)
런타임 정보 활용가능 (타입 정보, 실행 빈도, 분기 패턴 등)제한적 (PGO로 일부 가능)
플랫폼 독립성높음 (각 플랫폼에서 JIT 컴파일)낮음 (각 플랫폼별 별도 컴파일 필요)
배포 파일 크기작음 (바이트코드/중간 코드 배포)큼 (네이티브 코드 배포)
보안 측면약간 취약 (코드 변조 가능성)상대적으로 안전 (역공학이 어려움)
백그라운드 CPU 사용높음 (런타임 컴파일 작업)낮음 (컴파일 작업 없음)
배터리 영향 (모바일)높음 (컴파일 작업으로 인한 CPU 사용)낮음 (실행만 수행)
핫 리로딩 지원용이 (동적 코드 로딩 및 컴파일)어려움 (재컴파일 필요)
예측 가능성낮음 (컴파일 타이밍에 따른 성능 변동)높음 (일관된 실행 성능)
동적 언어 지원우수 (런타임 타입 정보 활용)제한적 (정적 분석의 한계)
실시간 시스템 적합성낮음 (예측 불가능한 컴파일 지연)높음 (예측 가능한 실행)
개발 주기 영향최소 (빠른 컴파일-실행 사이클)높음 (컴파일 대기 시간)
리플렉션/동적 기능완전 지원제한적 지원 (특별한 처리 필요)
장점동적 최적화로 장기간 실행 시 성능 상승, 실행 환경에 따른 최적화 가능빠른 시작, 예측 가능한 성능, 런타임 오버헤드 제거
단점초기 오버헤드 발생, 런타임 중 추가 컴파일로 인한 CPU/메모리 사용 증가빌드 시간이 길어짐, 플랫폼 종속성 존재, 동적 최적화 부족
사용 사례Java,.NET, 웹 브라우저의 JavaScript 엔진 등 동적 환경에 적합임베디드 시스템, 모바일 애플리케이션, 실시간 시스템 등 빠른 시작과 안정적인 성능이 필요할 때
주요 사용 예시Java, JavaScript, C#, Python(PyPy)C, C++, Rust, Go, Swift
하이브리드 접근ART(Android),.NET Native, GraalVMART(Android),.NET Native, GraalVM

참고 및 출처