루비를 기계어로 컴파일하기
(patshaughnessy.net)- 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_entry와 jit_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명령어를 추가로 컴파일해 같은 블록에 포함
- 첫 번째 YARV 명령어
- 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 도구가 있었다면 인기도 꽤 달랐을 것 같음
- MacRuby의 제작자가 Apple을 떠난 뒤 RubyMotion을 만들었음
-
Pat이 계속해서 프로젝트를 이어가는 걸 보니 정말 반가움
그의 첫 Ruby Under a Microscope 책과 블로그 글들은 내게 큰 영감을 줬음
예전에 Euruko 컨퍼런스에서 직접 만난 적도 있는데, 정말 훌륭한 사람이었음- 따뜻한 댓글에 감사함
-
처음 Ruby Under a Microscope를 읽었을 때 정말 재미있었음
그 덕분에 예전에 CTF 문제 풀이에도 활용했음
요즘 Ruby 내부 구현을 따라가진 못했지만, 새 버전이 나오면 꼭 살 생각임- 2002년부터 2010년까지 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는 다른 방식을 쓰는 것으로 보임
- 그게 바로 YJIT의 핵심임
-
동적 타입 언어를 JIT으로 빠르게 만드는 건 보통 메모리 사용량 증가라는 대가를 치름
Shopify 같은 대형 기업이 아니라면 이게 더 큰 문제일 수 있음- 하지만 작은 기업은 보통 애플리케이션 규모도 작음
요즘 클라우드 인스턴스는 코어당 4GiB 정도 메모리를 주기 때문에, 수백 MB의 JIT 코드 정도는 충분히 감당 가능함
- 하지만 작은 기업은 보통 애플리케이션 규모도 작음
-
YJIT이 함수 호출 횟수만 세서 핫스팟을 찾는 방식이 단순해 보였음
JavaScript JIT처럼 루프 내부의 무거운 연산을 감지하는 기능은 없을까 궁금했음
Ruby의 블록 구조가 이런 최적화에 도움이 될 수도 있을 것 같음- 맞음, Ruby는 루프 본문을 블록으로 처리하기 때문에
JIT이 블록을 별도 함수처럼 다루면서 반복문을 자연스럽게 최적화할 수 있음
이 부분은 다음 장에서 더 깊이 다뤄볼 예정임
- 맞음, Ruby는 루프 본문을 블록으로 처리하기 때문에