Roite 개발은 여러 이정표를 거쳐 진행되었습니다.
1단계: WebAssembly 기본 구현 (Gora Book 포팅)
- Rust로 작성된 WebAssembly 학습 서적인 “Gora Book”의 개념을 순수 Ruby로 포팅하며 시작되었습니다.
- WebAssembly 바이너리 파싱(프리앰블, 헤더, 섹션) 및 VM 실행 루프(명령어 패치-실행 반복)를 구현했습니다.
- 외부 세계와의 상호작용을 위해 최소한의
fd_write
함수를 구현하여 “Hello World” 출력을 성공시켰습니다.
2단계: WebAssembly 코어 스펙 기본 명령어 구현
- Ruby 실행에 대한 열망을 바탕으로, WebAssembly 코어 스펙의 기본 명령어 192개를 구현하는 데 집중했습니다.
i32
,i64
,f32
,f64
와 같은 4가지 숫자 타입에 대한 덧셈, 뺄셈 등 공통 명령어는 제너레이터를 활용하여 효율적으로 생성했습니다.
3단계: 그레이스케일 프로그램 실행 및 디버깅
- Rust로 작성된 그레이스케일 변환 프로그램을 실행하려 했으나 초기에는 실패했습니다.
- 메모리 할당 문제 해결 후 Rust 패닉이
unreachable
로 번역되는 점을 활용하여 디버깅을 진행했습니다. wasm-test-suite
를 통해i32
관련 테스트 케이스(특히 비트 시프트 연산)의 오류를 발견하고 수정하여 그레이스케일 프로그램 실행에 성공했습니다.
4단계: Ruby 실행을 위한 WASI (WebAssembly System Interface) 지원
- Ruby 실행을 위해서는 WebAssembly 명령어 외에 WASI 지원이 필수적임을 인지했습니다.
- 초기에는
ruby -v
명령어가 작동하도록 37개의 WASI 함수를 하나씩 구현했습니다(예:clock_time_get
을Time.now
로 래핑). - 인수 처리, 현재 시간, 난수 생성, 환경 변수, 파일 디스크립터 작업 등 기본적인 시스템 상호작용 기능들을 구현하여 Ruby의 내장 코어 라이브러리(예:
Integer#times
)가 작동하게 했습니다.
5단계: 파일 시스템 (Pre-opens) 구현
require
기능의 정상 작동을 위해 파일 시스템 인식이 필요했으며, 이를 위해 WASI의 “pre-opens” 메커니즘을 구현했습니다.- 보안상의 이유로 WebAssembly 런타임은 기본적으로 호스트 파일 시스템에 접근할 수 없으므로, VM 시작 시 미리 등록된 파일 디스크립터(3, 4, 5)를 통해 접근 가능한 경로를 설정하는 방식입니다.
wasi_populate_preopens
함수를 통해 게스트 경로와 호스트 경로 매핑을 등록하는 관련 함수들을 구현한 결과, Ruby 명령어가 로딩 경고 없이 정상적으로 실행되었습니다.
성능 최적화
- 블록 점프 최적화: WebAssembly 점프 명령어의 동적 타겟 계산 방식을 개선, 명령어 파싱 후 끝 위치를 미리 계산하고 캐싱하여 그레이스케일 벤치마크에서 실행 시간을 43% 단축했습니다.
- 인스턴스 생성 감소:
VM#set_id
,RbClass#new_instance
등에서 발생하는 과도한 객체 인스턴스 생성(그레이스케일에서 1,880만 개의 정수 객체 생성)을 줄이기 위해 자주 사용되는 정수 인스턴스를 메모이제이션하여 약 1초의 속도 향상을 이루었습니다. 궁극적으로는 즉시 값(immediate values) 사용이 목표입니다. - YJIT 활용: Ruby 3.1과 함께 YJIT를 사용했을 때 52%, Ruby 3.4에서는 57%의 실행 시간 단축 효과를 보여, 성능 향상에 YJIT가 매우 효과적임을 입증했습니다.