TECH 으로 돌아가기
TECH HACKER NEWS 오늘 8분 읽기 33 READS

Wasmi 2.0은 어떻게 가장 빠른 웹어셈블리 인터프리터가 됐나

웹어셈블리(Wasm)를 실행하는 방법은 크게 두 갈래다. 하나는 기계어로 컴파일해 돌리는 JIT/AOT 방식이고, 다른 하나는 바이트코드를 그대로 해석하는 인터프리터 방식이다. IoT 기기, 플러그인 시스템, 스마트 컨트랙트처럼 이식성과 작은 바이너리 크기, 빠른 시작 시간이 중요한 환경에서는 인터프리터가 여전히 현실적인 선택지다. Rust로 작성된 Wasmi는 그중에서도 널리 쓰이는 인터프리터로, Typst·Zellij 같은 플러그인 호스트나 Soroban·Ripple 같은 블록체인 런타임에 채택돼 왔다. 8개월간의 개편 끝에 공개된 Wasmi 2.0은 이번엔 특히 실행 성능에 초점을 맞췄다.

결과부터 보면, Apple M2 Pro에서 wasmi-benchmarks 스위트를 돌렸을 때 Wasmi 2.0은 1.0 대비 기하평균 약 2.2배 빨라졌다. 개발자는 이 버전이 Wasm3, Stitch 같은 가장 빠른 이식형 인터프리터들과 어깨를 나란히 한다고 설명한다. 흥미로운 점은 실행 속도를 끌어올리면서도 시작 성능은 이전 버전과 대체로 동등한 수준을 유지했다는 것이다. 인터프리터에서 실행과 시작은 종종 상충하는 지표인데, 두 마리 토끼를 함께 잡은 셈이다. 벤치마크는 세 종류의 하드웨어에서 측정됐고, 프로젝트 저장소를 통해 직접 재현할 수 있도록 공개돼 있다.

레지스터를 직접 쓰기 시작한 실행 엔진

성능 개선의 핵심은 값을 어디에 두고 계산하느냐에 있다. Wasmi 1.0은 명령어의 피연산자와 결과를 모두 스택 슬롯(스택상의 오프셋)으로 다뤘다. 값을 쓸 때마다 슬롯을 디코딩하고 로드·스토어하는 비용이 따라붙는다. 2.0은 여기에 ireg, freg32, freg64라는 누산기(accumulator) 레지스터를 도입해 피연산자와 결과를 실제 하드웨어 레지스터에서 읽고 쓰도록 바꿨다. 누산기는 암시적으로 지정되므로 값을 해독하거나 옮기는 과정이 필요 없어 코드가 단순하고 빨라진다.

다만 이 설계에는 대가가 있다. 결과가 항상 암시적 누산기로 들어가다 보니, 예전에는 명령어 하나로 끝나던 일이 이제는 값을 보존하기 위한 복사(copy) 명령을 추가로 요구한다. 개발자는 실제로 add나 load 뒤에 local.set·local.tee가 붙는 패턴이 유난히 많다는 점에 착안해, 결과를 누산기와 스택 슬롯에 동시에 저장하는 융합(fused) 명령어를 따로 만들었다. 제어 흐름에서도 block·loop·if 경계를 넘어 누산기 레지스터 값을 최대한 이어받게 해, 예컨대 반복문의 유도 변수가 레지스터에 머무를 수 있도록 했다. 반면 함수 호출에 같은 규칙을 적용하는 실험은 오히려 성능이 떨어져 병합하지 않았다.

인스턴스 접근을 한 번의 포인터 연산으로

또 하나의 큰 변화는 모듈 인스턴스의 내부 객체를 표현하는 방식이다. 1.0에서 global.get 같은 명령은 인스턴스에서 핸들 배열을 읽고, 인덱스로 핸들을 꺼낸 뒤, 다시 스토어에서 실제 객체를 조회하는 세 단계를 거쳤다. 각 단계가 앞 단계 결과에 의존하는 연쇄 로드라 비용이 컸고, 그래서 C 코드의 섀도 스택 포인터로 흔히 쓰이는 (global 0)만 특별히 빠르게 처리하는 예외 코드까지 두어야 했다. 2.0은 같은 모듈의 모든 인스턴스가 동일한 객체 레이아웃을 공유한다는 사실을 활용한다. 메모리·글로벌·테이블·함수·요소·데이터 순으로 정렬된 핸들 버퍼를 하나의 연속된 메모리에 담고, 인스턴티에이션 시점에 실제 객체 포인터를 미리 초기화해 둔다. 그 결과 인스턴스 객체 접근은 포인터 오프셋 한 번으로 끝난다.

이 대목에서 Wasmi의 설계 철학이 경쟁 인터프리터와 갈린다. Wasm3와 Stitch는 인스턴스별 바이트코드를 써서 객체 포인터를 바이트코드에 직접 박아 넣는다. 접근은 빠르지만 같은 모듈이라도 인스턴스마다 고유한 바이트코드를 따로 저장해야 한다. Wasmi는 모듈 단위 바이트코드를 유지해 같은 모듈의 모든 인스턴스가 하나의 IR을 공유하므로 메모리 소비가 크게 줄어든다. 2.0은 포인터를 코드에 심지 않고도 인스턴스 레이아웃 재설계만으로 Wasm3·Stitch급 접근 속도를 확보했다는 점이 핵심이다. 실제로 글로벌 변수만으로 카운트다운하는 counter-global 벤치마크에서 2.0은 (global 0)이든 (global 1)이든 고르게 빠른 반면, 1.0은 캐시가 걸리지 않는 (global 1)에서 크게 뒤처졌다. 같은 이유로 함수 호출도 개선됐는데, 1.0에서는 뮤텍스로 보호된 하나의 아레나에 매 호출마다 락을 걸고 인덱싱해야 했던 병목이 엔진 단위 함수 주소로 대체됐다.

실무자가 눈여겨볼 지점과 한계

저수준 튜닝도 눈에 띈다. 2.0의 명령어 핸들러는 9개 인자 중 7개를 범용 레지스터(GPR)로 넘기려 하는데, sysv64 호출 규약은 정수 인자용 GPR을 6개까지만 제공한다. 7번째 정수 인자는 매 디스패치마다 스택으로 밀려나 성능을 갉아먹는다. 개발자는 비교적 비싼 연산에만 쓰이는 instance 인자를 부동소수점 값으로 바꿔 이 제약을 우회했고, 정수-부동소수 레지스터 이동 비용은 벤치마크상 미미했다고 밝혔다. 아직 불안정한 preserve_none ABI가 안정화되면 상황이 더 나아질 여지가 있다. 명령어 디스패치 방식도 네 가지를 지원하며, 가능한 경우 스레디드 코드 기반 구성을 자동 선택하는 auto-dispatch 기능이 들어갔다.

성능 외에도 실무에서 반길 변화가 있다. validate 크레이트 기능으로 검증 로직을 떼어내 바이너리 크기를 크게 줄일 수 있고, 안정화된 연료 계측(fuel metering), 웹어셈블리 결정론적 프로파일 지원, 개선된 CLI 도구가 포함됐다. 스마트 컨트랙트나 샌드박스 실행처럼 실행 비용을 정밀하게 제어해야 하는 환경에서는 안정적 연료 계측과 결정론적 프로파일이 특히 유용하다. 다만 이 발표는 개발자 본인이 진행한 벤치마크 결과라는 점, 그리고 세 종류의 하드웨어에서 인터프리터별 선호가 갈렸다고 언급된 만큼 워크로드와 아키텍처에 따라 결과가 달라질 수 있다는 점은 감안할 필요가 있다. 전체 런타임별 상세 결과는 후속 글에서 다룰 예정이라고 밝혔으므로, 도입을 검토한다면 자신의 실제 워크로드로 재현 벤치마크를 돌려보는 편이 안전하다.

SOURCE · HACKER NEWS
원문 전체 보기 → https://wasmi-labs.github.io/blog/posts/wasmi-v2.0/
SHARE
NEXT · CHOOSE

변화를 읽었다면,
내가 만들 수익 구조를 고릅니다.

정보를 더 모으는 데서 멈추지 않고, 광고·외주·판매·중개·구독 중 내 상황에 맞는 출발점을 정해보세요.

21가지 수익 구조 살펴보기
처리 중...