1P by GN⁺ 16일전 | ★ favorite | 댓글 1개
  • 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으로 언급되지만, 세부 내용은 본문에 없음
Hacker News 의견
  • 예전에 MacRuby가 LLVM을 이용해 macOS에서 네이티브 코드로 컴파일되고, Objective‑C 프레임워크와 통합되던 시절이 있었음
    꽤 멋진 아이디어였는데, 결국 Apple이 Swift로 방향을 바꾼 듯함
    새 버전이 나오면 Ruby Under a Microscope 책을 꼭 사서 읽어볼 생각임. Ruby는 여전히 좋아하지만 실제로 쓸 기회가 많지 않았음

    • MacRuby의 제작자가 Apple을 떠난 뒤 RubyMotion을 만들었음
      지금은 다른 사람들이 이어가고 있지만, 현재는 DragonRuby(게임 중심 Ruby 구현체)에 더 집중하는 분위기임
    • MacRuby는 저자가 떠난 후 RubyMotion으로 이어졌음
      참고로 위키 문서도 있음
    • 지금도 Objective‑C를 써서 macOS, iOS, iPadOS용 앱을 만들 수 있음
      다만 예전 API들은 더 이상 지원되지 않을 수도 있음
    • 여러 언어를 다뤄온 입장에서 보면, Apple이 Swift로 옮긴 건 마치 Microsoft가 VB6에서 VB.Net으로 넘어간 것과 비슷한 느낌임
      VB6은 개발 속도가 정말 빨랐고, Direct3D나 ASP Classic까지 다룰 수 있었음
      Ruby의 우아함과 개발 편의성이 그 시절을 떠올리게 함
      만약 Ruby에 VB6 수준의 GUI 도구가 있었다면 인기도 꽤 달랐을 것 같음
  • Pat이 계속해서 프로젝트를 이어가는 걸 보니 정말 반가움
    그의 첫 Ruby Under a Microscope 책과 블로그 글들은 내게 큰 영감을 줬음
    예전에 Euruko 컨퍼런스에서 직접 만난 적도 있는데, 정말 훌륭한 사람이었음

    • 따뜻한 댓글에 감사함
  • 처음 Ruby Under a Microscope를 읽었을 때 정말 재미있었음
    그 덕분에 예전에 CTF 문제 풀이에도 활용했음
    요즘 Ruby 내부 구현을 따라가진 못했지만, 새 버전이 나오면 꼭 살 생각임

    • 2002년부터 2010년까지 Ruby를 많이 썼는데, 이후엔 거의 손을 놓았음
      이번 글을 보고 새 버전 책을 다시 읽고 싶어졌음
  • Ruby 컴파일 얘기가 나와서 말인데, Stripe 개발자들이 만든 Sorbet compiler를 써본 적 있는지 궁금함
    Sorbet Compiler 공개 글

    • 지금은 저장소에서 사라졌고, 더 이상 개발되지 않는 듯해서 아쉬움
      AOT 컴파일은 Ruby에선 정말 어려움
      Sorbet의 접근이 흥미로운 이유는 Ruby의 타입 검사를 기반으로 빠른 경로를 만들 수 있기 때문임
      나도 개인 프로젝트로 Ruby 컴파일러를 만들고 있는데, hokstad.com/compiler
      writing-a-compiler-in-ruby를 참고하고 있음
      지금은 RubySpec 통과에 집중하고 있고, 나중엔 타입 기반 최적화도 시도해볼 생각임
  • Ruby 컴파일과는 직접 관련 없지만, Enterprise Integration with Ruby 책이 웹 외의 영역에서 Ruby를 활용하는 데 큰 통찰을 줬음

  • MRuby를 알게 된 이후로, 내 프로젝트와 스크립트를 독립 실행 파일로 바꾸는 재미에 빠져 있음

  • Ruby Under a Microscope가 여전히 업데이트되고 있어서 기쁨
    Ruby 내부 동작을 이해하려는 사람에게는 필독서라고 생각함

  • YJIT 블록이 여러 번 실행될 때, 입력 타입별로 어떻게 컴파일을 추적하는지 궁금했음
    Ruby가 int나 float 등 다양한 타입을 어떻게 처리하는지 알고 싶음

    • 그게 바로 YJIT의 핵심
      실제 타입이 제공될 때까지 컴파일을 미루는 “wait‑and‑see” 접근을 사용함
      각 타입별로 블록 버전을 따로 관리하고, 상황에 맞게 호출함
      이 알고리즘은 Basic Block Versioning이라 불림
      Shopify의 Maxime Chevalier‑Boisvert가 RubyConf 2021 발표 영상에서 잘 설명함
      새 JIT 엔진인 ZJIT는 다른 방식을 쓰는 것으로 보임
  • 동적 타입 언어를 JIT으로 빠르게 만드는 건 보통 메모리 사용량 증가라는 대가를 치름
    Shopify 같은 대형 기업이 아니라면 이게 더 큰 문제일 수 있음

    • 하지만 작은 기업은 보통 애플리케이션 규모도 작음
      요즘 클라우드 인스턴스는 코어당 4GiB 정도 메모리를 주기 때문에, 수백 MB의 JIT 코드 정도는 충분히 감당 가능함
  • YJIT이 함수 호출 횟수만 세서 핫스팟을 찾는 방식이 단순해 보였음
    JavaScript JIT처럼 루프 내부의 무거운 연산을 감지하는 기능은 없을까 궁금했음
    Ruby의 블록 구조가 이런 최적화에 도움이 될 수도 있을 것 같음

    • 맞음, Ruby는 루프 본문을 블록으로 처리하기 때문에
      JIT이 블록을 별도 함수처럼 다루면서 반복문을 자연스럽게 최적화할 수 있음
      이 부분은 다음 장에서 더 깊이 다뤄볼 예정임