본문 바로가기

카테고리 없음

RN을 준비하며 AOT와 JIT 가 무엇인지?

AOT와 JIT의 기본 개념

AOT(Ahead-of-Time) 컴파일은 프로그램 실행 전에 미리 기계어로 변환하는 방식이고, JIT(Just-in-Time) 컴파일은 프로그램 실행 중에 필요한 부분을 실시간으로 컴파일하는 방식입니다.

JIT 컴파일 과정

1단계: 소스 코드 → 중간 코드 변환

  • Java의 경우: .java → .class (바이트코드)
  • C#의 경우: .cs → .dll/.exe (IL 코드)
  • 이 단계에서는 아직 기계어가 아닌 중간 표현 형태로 변환

2단계: 런타임 로딩

  • 프로그램 실행 시 가상머신(JVM, .NET CLR 등)이 중간 코드를 메모리에 로드
  • 메타데이터와 함께 프로그램 구조 파악

3단계: 인터프리터 실행

  • 초기에는 중간 코드를 인터프리터가 한 줄씩 해석하여 실행
  • 실행 속도는 느리지만 즉시 시작 가능

4단계: 핫스팟 탐지

  • 자주 실행되는 코드 블록(hot spot) 식별
  • 실행 빈도, 루프 횟수 등을 기준으로 판단
  • 임계값을 넘으면 컴파일 대상으로 선정

5단계: 프로파일링

  • 실행 중인 코드의 특성 분석
  • 변수 타입, 메소드 호출 패턴, 분기 예측 정보 수집
  • 최적화에 필요한 런타임 정보 축적

6단계: 최적화된 기계어 컴파일

  • 수집된 프로파일 정보를 바탕으로 최적화 수행
  • 인라이닝, 루프 최적화, 데드 코드 제거 등 적용
  • 타겟 CPU 아키텍처에 맞는 기계어 생성

7단계: 코드 캐싱 및 실행

  • 컴파일된 기계어를 메모리에 캐싱
  • 이후 동일한 코드 실행 시 컴파일된 버전 사용
  • 필요시 디옵티마이제이션으로 되돌리기도 함

JPT의 답변을 듣고 궁금한점 인터프리터 / Optimizing Compiler가 뭐야?

인터프리터(Interpreter)

 

기본 동작 원리

인터프리터는 소스 코드나 중간 코드를 한 줄씩 읽어서 즉시 실행하는 방식입니다.

인터프리터의 세부 동작 과정

1단계: 코드 파싱

  • 소스 코드를 토큰으로 분해
  • 구문 분석을 통해 추상 구문 트리(AST) 생성
  • 의미 분석으로 타입 검사, 변수 바인딩 등 수행

2단계: 실행 환경 준비

  • 심볼 테이블 생성 (변수, 함수 등의 정보 저장)
  • 스택 프레임 초기화
  • 메모리 할당 및 가비지 컬렉터 설정

3단계: 명령어별 실행

 
while (instruction = getNextInstruction()) {
    switch (instruction.type) {
        case LOAD_VARIABLE:
            value = symbolTable.lookup(instruction.name);
            stack.push(value);
            break;
        case ADD:
            b = stack.pop();
            a = stack.pop();
            stack.push(a + b);
            break;
        case CALL_FUNCTION:
            executeFunction(instruction.functionName);
            break;
    }
}

4단계: 런타임 타입 체크

  • 매 연산마다 피연산자의 타입 확인
  • 동적 타입 변환 수행
  • 메모리 경계 검사

인터프리터의 종류

트리 워킹 인터프리터(Tree-Walking Interpreter)

  • AST를 직접 순회하며 실행
  • 구현이 간단하지만 성능이 느림
  • 재귀적 함수 호출로 각 노드 처리

바이트코드 인터프리터(Bytecode Interpreter)

  • 소스 코드를 바이트코드로 컴파일 후 실행
  • 스택 기반 가상 머신에서 동작
  • Python, Java 초기 버전에서 사용

스레드 코드 인터프리터(Threaded Code Interpreter)

  • 각 명령어를 함수 포인터로 표현
  • 함수 호출 오버헤드를 줄여 성능 향상
  • Forth 언어에서 주로 사용

최적화 컴파일러(Optimizing Compiler)

기본 개념

최적화 컴파일러는 프로그램의 성능을 향상시키기 위해 다양한 최적화 기법을 적용하여 기계어를 생성합니다.

최적화 컴파일러의 단계별 과정

1단계: 중간 표현(IR) 생성

  • 고수준 언어를 중간 표현으로 변환
  • SSA(Static Single Assignment) 형태로 변환
  • 제어 흐름 그래프(CFG) 구성

2단계: 분석 단계

 
데이터 흐름 분석:
- 변수의 정의와 사용 지점 파악
- 도달 가능성 분석
- 생존 변수 분석

제어 흐름 분석:
- 루프 구조 식별
- 지배 관계 분석
- 분기 예측 정보 수집

3단계: 최적화 기법 적용

로컬 최적화

  • 상수 접기(Constant Folding): 3 + 5 → 8
  • 상수 전파(Constant Propagation): int x = 5; int y = x * 2; → int y = 10;
  • 공통 부분식 제거: a * b + c * d + a * b → temp = a * b; temp + c * d + temp

글로벌 최적화

  • 데드 코드 제거: 사용되지 않는 변수나 코드 삭제
  • 루프 불변 코드 이동: 루프 내 변하지 않는 계산을 루프 밖으로 이동
 
c
// 최적화 전
for (int i = 0; i < n; i++) {
    result[i] = array[i] * (x + y);
}

// 최적화 후
int temp = x + y;
for (int i = 0; i < n; i++) {
    result[i] = array[i] * temp;
}

고급 최적화

  • 인라이닝(Inlining): 함수 호출을 함수 본체로 대체
  • 루프 언롤링(Loop Unrolling): 루프 반복 횟수 줄이기
  • 벡터화(Vectorization): SIMD 명령어 활용

4단계: 레지스터 할당

  • 변수를 CPU 레지스터에 효율적으로 할당
  • 그래프 컬러링 알고리즘 사용
  • 스필(Spill) 코드 생성으로 메모리 접근 최소화

5단계: 기계어 생성

  • 타겟 아키텍처에 맞는 명령어 선택
  • 명령어 스케줄링으로 파이프라인 최적화
  • 페어링 규칙 적용으로 병렬 실행 극대화

최적화 레벨

O0 (최적화 없음)

  • 컴파일 속도 최우선
  • 디버깅 정보 보존

O1 (기본 최적화)

  • 로컬 최적화 중심
  • 컴파일 시간과 성능의 균형

O2 (표준 최적화)

  • 대부분의 최적화 기법 적용
  • 실제 배포에서 주로 사용

O3 (공격적 최적화)

  • 모든 최적화 기법 적용
  • 코드 크기 증가 가능성

인터프리터 vs 최적화 컴파일러 비교

성능 특성

 
실행 시간 = 컴파일 시간 + 런타임

인터프리터:
- 컴파일 시간: 0 (즉시 실행)
- 런타임: 매우 느림 (매번 파싱/해석)

최적화 컴파일러:
- 컴파일 시간: 길음 (복잡한 분석/최적화)
- 런타임: 매우 빠름 (최적화된 기계어)

메모리 사용량

인터프리터: 소스 코드 + 실행 상태 + 런타임 시스템 컴파일러: 최적화된 기계어 (일반적으로 더 효율적)

개발 생산성

인터프리터: 즉시 실행 가능, 동적 기능 지원 컴파일러: 컴파일 단계 필요, 정적 분석으로 오류 조기 발견

현대적 하이브리드 접근법

계층적 컴파일(Tiered Compilation)

  1. 인터프리터로 즉시 실행 시작
  2. 자주 사용되는 코드를 빠른 컴파일러로 컴파일
  3. 핫스팟은 최적화 컴파일러로 재컴파일

적응적 최적화(Adaptive Optimization)

  • 런타임 프로파일 정보를 활용
  • 실행 패턴에 따라 동적으로 최적화 수준 조정
  • 추측 기반 최적화 후 검증

이러한 하이브리드 방식은 인터프리터의 빠른 시작과 컴파일러의 높은 성능을 모두 활용할 수 있게 해줍니다.

 

AOT JIT의 등장 배경

전통적인 JIT의 단점을 보완하기 위해 AOT JIT가 개발되었습니다:

  • 콜드 스타트 문제: 초기 실행 시 컴파일 시간으로 인한 지연
  • 웜업 시간: 최적화되기까지의 시간 소요
  • 메모리 오버헤드: 런타임 컴파일러와 메타데이터 유지

AOT JIT 구현 방식

프로파일 기반 AOT (Profile-Guided AOT)

  • 사전에 프로파일 정보를 수집하여 AOT 컴파일 시 활용
  • 실제 사용 패턴을 반영한 최적화 가능

하이브리드 접근법

  • 중요한 코드는 AOT로 미리 컴파일
  • 나머지는 런타임에 JIT 컴파일
  • GraalVM의 Native Image가 대표적 예시

계층적 컴파일 (Tiered Compilation)

  • 여러 최적화 레벨을 단계적으로 적용
  • 빠른 초기 컴파일 후 점진적 최적화

주요 장단점

장점:

  • 빠른 시작 시간과 초기 성능
  • 예측 가능한 성능 특성
  • 메모리 사용량 절약

단점:

  • 런타임 정보 부족으로 인한 최적화 한계
  • 컴파일 시간 증가
  • 동적 기능 제약