루비를 기계어로 컴파일하기

3 weeks ago 10

  • YJIT과 ZJIT는 Ruby 3.x에서 루비 코드를 기계어로 변환해 실행 속도를 높이는 JIT 컴파일러 구조
  • YJIT은 각 함수나 블록 호출 횟수를 카운트해 일정 임계값에 도달하면 해당 코드를 기계어로 변환
  • 변환된 코드는 YJIT 블록에 저장되며, 각 블록은 여러 YARV 명령어를 대응하는 ARM64 기계어 명령어로 변환
  • Branch Stub을 사용해 런타임에 실제 데이터 타입을 관찰하고, 그에 맞는 기계어 명령어를 선택적으로 생성
  • 이러한 구조는 Ruby의 실행 성능 향상과 동적 타입 처리 효율성을 동시에 달성하기 위한 핵심 메커니즘

Chapter 4: 루비를 기계어로 컴파일하기

Interpreting vs. Compiling Ruby Code

  • 원문에 세부 내용 없음

Counting Method and Block Calls

  • YJIT은 프로그램의 함수 및 블록 호출 횟수를 추적해 핫스팟 코드를 식별
    • 각 함수나 블록의 YARV 명령어 시퀀스 옆에 jit_entryjit_entry_calls 값을 저장
    • jit_entry는 초기에는 null이며, 나중에 YJIT이 생성한 기계어 코드의 포인터를 저장
    • jit_entry_calls는 호출될 때마다 1씩 증가
  • 호출 횟수가 임계값에 도달하면 YJIT이 해당 코드를 기계어로 컴파일
    • Ruby 3.5의 기본 임계값은 작은 프로그램 30회, 대규모 애플리케이션 120회
    • 실행 시 --yjit-call-threshold 옵션으로 변경 가능
  • 이 방식으로 YJIT은 자주 실행되는 코드만 기계어로 변환해 효율적 실행 경로 확보

YJIT Blocks

  • YJIT은 생성한 기계어 명령어를 YJIT 블록에 저장
    • YJIT 블록은 Ruby 블록과 다르며, YARV 명령어의 일부 구간을 대응
    • 각 Ruby 함수나 블록은 여러 YJIT 블록으로 구성
  • 예시 프로그램에서 블록이 30번째 실행될 때 YJIT이 컴파일을 시작
    • 첫 번째 YARV 명령어 getlocal_WC_1을 기계어로 변환해 새로운 YJIT 블록 생성
    • 이후 getlocal_WC_0 명령어를 추가로 컴파일해 같은 블록에 포함
  • Figure 4-8에 따르면, YJIT은 ARM64 명령어를 생성해 M1 프로세서의 x1, x9 레지스터에 값을 로드
    • getlocal_WC_1은 이전 스택 프레임의 지역 변수를, getlocal_WC_0은 현재 스택의 변수를 스택에 저장
    • 생성된 기계어 명령어는 동일한 동작을 수행

YJIT Branch Stubs

  • YJIT이 opt_plus 명령어를 컴파일할 때 피연산자 타입을 알 수 없는 문제 발생
    • 정수, 문자열, 부동소수점 등 타입에 따라 필요한 기계어 명령어가 다름
    • 예: 정수 덧셈은 adds 명령어 사용, 부동소수점 덧셈은 다른 명령어 필요
  • 이를 해결하기 위해 YJIT은 사전 분석 대신 런타임 관찰 방식을 사용
    • 프로그램 실행 중 실제 전달된 값의 타입을 확인해 그에 맞는 기계어를 생성
  • 이 동작을 위해 Branch Stub을 사용
    • 새로운 분기(branch)가 아직 연결된 블록이 없을 때, 임시로 stub에 연결
    • 이후 실제 타입이 확인되면 해당 stub을 적절한 블록으로 대체

ZJIT (언급만 있음)

  • 목차에 ZJIT 관련 섹션이 포함되어 있으나, 본문에 구체적 설명 없음

요약

  • YJIT은 Ruby 3.5에서 동적 타입 언어의 실행 효율을 높이기 위한 JIT 컴파일러
  • 호출 횟수 기반 컴파일 트리거, YJIT 블록 구조, Branch Stub을 통한 런타임 타입 확인이 핵심
  • ARM64 아키텍처에서 실제 기계어 명령어로 변환해 루비 코드의 실행 속도 향상
  • ZJIT은 차세대 JIT으로 언급되지만, 세부 내용은 본문에 없음

Read Entire Article