전체 글

Wasmi 2.0 릴리스, WebAssembly 인터프리터 1.0 대비 2.2배 빨라졌다

xXfacebook페이스북

WebAssembly 인터프리터 Wasmi의 2.0이 2026년 9월 1일에 나왔습니다. Wasmi 1.0 대비 벤치마크 기하평균 약 2.2배 빨라졌습니다(Apple M2 Pro, wasmi-benchmarks 스위트 기준). Typst, Zellij, Stellar의 스마트컨트랙트 Soroban 같은 프로젝트가 쓰는 인터프리터라 이 숫자는 그것들을 임베딩한 쪽의 실행 비용에 직결됩니다.

이 릴리스가 다시 눈에 띄게 된 건 2026년 9월 14일자 긱뉴스 토픽입니다. 다만 이 글의 중심은 속도가 아니라 바뀐 것의 목록입니다. 2.0으로 옮기는 임베더가 손봐야 할 지점 — fuel metering 재보정, 이름이 바뀌었는데 컴파일 에러만으로 걸러지지 않는 API 치환, 서브커맨드가 필수가 된 CLI — 가 발표문과 마이그레이션 문서에 흩어져 있어 한 자리에 모았습니다. 발표문(Robin Freyler 명의, 2026-09-01), GitHub 릴리스 노트, 마이그레이션 가이드, crates.io 메타데이터를 겹쳐 읽은 정리이고 Wasmi 2.0을 직접 실행해 확인한 내용은 아닙니다.

결론부터

성능 행은 전부 Wasmi 1.0 기준 상대값이고 Table 메모리 행의 출처는 발표문이 아니라 GitHub 릴리스 노트입니다.

항목 Wasmi 1.0 Wasmi 2.0
실행 속도 기준 기하평균 약 2.2배 (Apple M2 Pro, wasmi-benchmarks)
Table 메모리 기준 2~4배 감소 (GitHub 릴리스 노트)
시작 성능 기준 대체로 동등 ("mostly on par")
임베더가 고칠 것 — fuel 재보정, Ref 치환 확인, CLI 서브커맨드, 기본 피처 점검

2.2배는 벤치마크를 돌린 세 하드웨어 가운데 Apple M2 Pro에서 나온 수치이고 AMD EPYC 7763, Intel Xeon Platinum 8370C에서 측정한 값도 함께 올라와 있습니다. 다만 원문 텍스트가 기기별 수치를 배정하지 않아 이 글에서는 옮기지 않았습니다. 비교 대상이 1.0.x 중 어느 패치 버전인지는 원문이 밝히지 않아 이 글도 "Wasmi 1.0"이라는 표기를 그대로 씁니다.

Wasmi 1.0과 Wasmi 2.0의 성능을 나란히 비교한 표. 벤치마크 기하평균은 약 2.2배 빨라졌고, 시작 성능은 이전 버전과 대체로 동등하다는 항목이 함께 놓여 있다.

기하평균 2.2배는 세 벤치 하드웨어 가운데 Apple M2 Pro 수치다.

왜 빨라졌는가

저자 Robin Freyler는 이번 2.0을 8개월간의 집중 작업으로 소개합니다. 그 가운데 하나만 꼽은 최적화는 러스트 컴파일러 쪽 변화와 맞물린 것입니다. Rust 1.92가 MIR 패스 가운데 DestinationPropagation을 기본으로 켜면서 Wasmi의 디스패치 경로가 단일 csel 분기점으로 병합됐고 Wasmi 2.0은 그에 맞춘 수정을 넣었습니다. csel은 조건에 따라 두 값 중 하나를 고르는 기계 명령입니다. 저자는 이 수정을 2.0의 "the single most important 'optimization'", 즉 가장 중요한 최적화 하나로 꼽습니다.

CoreMark 점수가 이 수정을 전후로 약 2800에서 4200 이상으로 올랐습니다(약 +50%). 주의할 것은 2800이 Wasmi 1.0의 점수가 아니라는 점입니다. 원문은 "After applying the fix to Wasmi 2.0 its CoreMark score rose from ~2800 to over 4200"으로, 수정 전 Wasmi 2.0의 점수와 수정 뒤의 점수를 잇고 있습니다. 이 병합이 성능에 나쁜 이유는 분기점 하나에 서로 다른 명령의 실행 히스토리가 섞여 분기 예측기가 그 히스토리를 보게 되기 때문이고, 그 개선은 tail call 기반 디스패치 구성에서만 나타납니다.

전부 빨라진 것은 아닙니다. 시작(startup) 성능은 이전 버전과 대체로 동등하다는 표현("mostly on par")이 원문의 수준입니다. "동일"이 아니라 "대체로"이고 이 단어를 떼고 옮기면 과장이 됩니다.

디스패치 네 가지 방식

인터프리터의 성능은 다음 명령을 고르는 디스패치 방식에서 갈립니다. Wasmi 2.0은 네 가지 방식을 갖추고 있고 기본값 auto-dispatch는 tail call 지원이 확인된 대상에서만 threaded code 방식을 쓰고, 그렇지 않으면 portable 폴백으로 갑니다. portable-dispatch 피처를 직접 설정하면 그 설정이 우선합니다.

방식 켜는 법 속도와 비고
Direct-Threaded Code 기본 (auto-dispatch) 가장 빠릅니다. IR에 함수 포인터를 직접 심고 tail call로 넘어갑니다. Wasm3·Stitch도 같은 방식
Indirect-Threaded Code indirect-dispatch DTC 대비 10~15% 느립니다. 대신 IR 메모리를 아낍니다
Switch-Loop portable-dispatch + indirect-dispatch Wasmi 1.0의 방식입니다. tail call을 지원하지 않는 플랫폼용
Call-Loop portable-dispatch 단독 매우 느리고 메모리 효율도 나빠 비추천입니다. 선택지라기보다 피처 조합이 만들어 내는 폴백

표에서 주의할 곳은 마지막 줄입니다. Call-Loop은 선택지처럼 보이지만 portable-dispatch만 켠 피처 조합에서 결과적으로 나오는 방식이고, 원문의 분류도 very slow and not memory efficient, 즉 느릴 뿐 아니라 메모리 효율도 나쁘다는 것입니다.

Wasmi 2.0의 디스패치 네 가지 방식을 속도·메모리·비고와 함께 비교한 표. Direct-Threaded Code가 가장 빠르고, Indirect-Threaded Code는 10~15% 느린 대신 IR 메모리를 아끼며, Switch-Loop은 tail call이 없는 플랫폼용, Call-Loop은 매우 느리고 메모리 효율도 나빠 비추천으로 표시돼 있다.

네 줄 가운데 세 줄은 옵션이고 Call-Loop은 피처 조합이 만들어 내는 폴백이다.

고정 64비트 셀과 메모리 변화

실행 모델은 누산기 방식입니다. 정수용 ireg와 부동소수점용 freg32, freg64 세 레지스터가 있습니다. 이 구조는 Wasm3·Stitch와 비슷합니다.

SIMD를 둘러싼 설계 결정이 이번 버전에서 실질적인 차이를 만듭니다. 옛 설계는 SIMD 값을 담으려고 셀을 128비트로 넓혔고 그 대가로 메모리가 늘고 성능이 510% 손해를 봤습니다. 이 비용은 옛 설계에서 simd 피처를 켠 경우에 붙던 것입니다. 2.0은 셀을 고정 64비트로 유지하므로 simd 피처를 켜도 메모리 증가도 이 성능 손해도 없습니다. 방향을 뒤집어 "2.0에서 SIMD는 510% 느리다"로 쓰면 정반대가 됩니다.

내부 구조도 바뀌었습니다. 코드가 놓이는 CodeMap은 1.0에서 뮤텍스로 보호되는 arena였는데 2.0에서는 append-only buckets로 바뀌어 수명 동안 재할당이 없고 호출이 lock-free가 됐습니다. Wasm-to-Wasm 호출은 Wasm3·Stitch 수준을 유지합니다.

릴리스 노트 쪽에 적힌 수치도 두 개 있습니다. Table 메모리가 32비트 플랫 참조로 2~4배 줄어든 것(마이그레이션 가이드에도 나옵니다)과 InstanceEntity 재설계로 count·globals 벤치마크가 약 35% 개선된 것입니다.

128비트로 셀을 넓히던 옛 설계와 고정 64비트 셀을 유지하는 Wasmi 2.0 설계를 비교한 도식. 옛 설계에서 simd 피처를 켜면 메모리 증가와 5~10% 성능 손해가 따라붙던 것과 달리, 2.0에서는 같은 피처를 켜도 두 비용이 없다는 대비를 보여준다.

SIMD 비용은 사라지고 셀 폭은 64비트에 고정됐다.

1.0에서 2.0으로 옮길 때

마이그레이션 가이드(docs/migration-v1-to-v2.md)가 정리한 항목을 옮깁니다. 가장 먼저 손댈 것은 fuel metering입니다. 가이드의 문장은 이렇습니다.

"Wasmi v2 replaces the old fuel metering that was tied to Wasmi's internal bytecode with a stable fuel metering that is tied to the input Wasm bytecode instead."

계량 기준이 내부 바이트코드에서 입력 Wasm 바이트코드로 옮겨 갔다는 뜻이고 가이드는 "Absolute fuel numbers therefore differ from Wasmi 1.x"라고 이어서 적습니다. 연료량을 저장하거나 비교하거나 상수로 박아 둔 코드는 재보정이 필요합니다. 연산 종류별 비용을 정하는 새 API Config::operator_cost와 Config::fuel_cost가 함께 들어왔고 같은 방식은 Wasmtime도 씁니다. 여기서 "stable fuel metering"이라는 말은 "기능이 안정화됐다"는 뜻이 아니라 **fuel 비용이 버전 사이에 상대적으로 안정적으로 유지된다(대신 정밀도는 떨어진다)**는 뜻입니다.

두 번째로 조심할 것은 Ref → Nullable 이름 변경입니다. 가이드의 경고 문장은 이렇습니다.

"…so this rename does not produce compile errors at all use sites and requires attention"

여기서 부정이 걸리는 범위를 정확히 읽어야 합니다. v2 에도 Ref라는 이름이 남아 있지만 그것이 가리키는 것은 완전히 다른 타입이라, 이 이름 변경이 모든 사용 지점에서 컴파일 에러를 만들지는 않습니다. 가이드가 든 v1 코드(wasmi::Ref<wasmi::Func>, Ref::Val(func))는 2.0에서 실제로 에러가 나고, 에러가 나지 않고 통과하는 사용 지점도 있습니다. 새 Ref는 제네릭이 아닌 완전히 다른 enum이고 바뀐 치환은 Ref::Val(func) → Nullable::Val(func)입니다. 컴파일 에러만으로는 걸러지지 않으므로 코드베이스에서 Ref를 직접 찾아 확인해야 하는 항목입니다.

타입 시그니처도 바뀌었습니다. TableType::new의 첫 인자가 ValType::FuncRef에서 RefType::Func로 바뀌었고 new64도 같습니다.

// 1.x
TableType::new(ValType::FuncRef, 10, Some(100))
// 2.0
TableType::new(RefType::Func, 10, Some(100))

ResourceLimiter의 memory_grow_failed는 Result<(), LimiterError>를 반환하도록 바뀌었고 에러 타입은 wasmi::errors에 모여 있습니다. StoreLimits의 trap_on_grow_failure는 이전에는 조용히 무시되던 동작이었는데 이제는 패닉을 강제합니다. 이 플래그로 제한을 걸어 뒀다면 결과가 달라지는 지점입니다.

CLI도 그대로 쓸 수 없습니다. 바이너리 이름이 wasmi_cli에서 wasmi로 바뀌었고 서브커맨드가 필수가 됐습니다. v128 인자는 거부됩니다.

wasmi run app.wasm --invoke main
wasmi wast tests.wast

기본 피처 목록은 default = ["stable", "std", "wat", "validate", "memory64", "auto-dispatch"]입니다. --no-default-features로 빌드하던 프로젝트는 필요한 피처를 직접 다시 켜야 합니다. validate를 끄면 바이너리가 약 200–300kB 줄어드는데 Wasm 입력을 온전히 통제할 수 있을 때만 끌 수 있는 옵션입니다. 워크스페이스 전체가 Rust 2024 에디션으로 옮겨 갔지만 MSRV는 그대로라 사용자 쪽 영향은 없습니다.

확인 목록은 이 정도입니다.

  • fuel 수치를 저장·비교·상수로 두는 코드
  • Ref 를 쓰는 곳 — 컴파일 에러만으로 걸러지지 않으므로 직접 검색
  • TableType::new / new64 호출부
  • memory_grow_failed 구현과 trap_on_grow_failure에 의존하던 동작
  • wasmi_cli 를 부르던 스크립트와 실행 명령
  • --no-default-features 빌드의 피처 목록

Wasmi 1.0에서 2.0으로 옮길 때 밟는 순서를 이은 흐름도. fuel metering 재보정에서 시작해 기본 피처 재선택, Ref에서 Nullable로의 API 치환 확인, CLI 서브커맨드 변경까지 단계가 화살표로 이어져 있다.

컴파일 에러만으로는 걸러지지 않는 항목이 가장 조용히 지나간다.

버전 이력과 헷갈리기 쉬운 표현

crates.io의 버전 이력(2026-09-14 API 조회)이 이번 2.0이 어디에 놓이는지를 정리해 줍니다. 1.0.0(2025년 12월 3일 게시)과 이어 나온 1.0.1(2025년 12월 5일 게시)이 yank 돼 있고 1.0.2부터 1.0.9까지는 yank 되지 않았습니다. yank 시점과 사유는 crates.io API 가 알려 주지 않습니다. 2.0 직전의 안정판은 2026년 6월 12일자 1.1.0입니다. 2.0.0-beta.7부터 beta.10까지의 프리릴리스가 2026년 7월 9일부터 8월 10일 사이에 나왔고 2.0.0은 2026년 9월 1일부터 latest로 붙어 있습니다.

규모 지표도 있습니다. 누적 다운로드는 22,819,056회, 최근 90일 기준은 4,043,275회입니다(2026-09-14 crates.io API 조회). 조회 시점마다 달라지는 값이라 조회일을 적어 둡니다.

쓰이는 곳은 발표문이 열거해 있습니다. 앞서 든 세 곳 외에 플러그인 시스템 Josh, Ripple, 게임 콘솔 Firefly Zero, IoT와 클라우드 호스트입니다. 후원도 배경에 있습니다. Stellar Development Foundation이 2024년 10월부터 후원해 왔고 저자는 후원이 없었다면 Wasmi 2.0은 존재하지 않았을 것이라고 적었습니다.

다음 버전 방향도 발표문에 있습니다. Wasmi 3.0의 목표는 WebAssembly 3.0 전체 지원이고 그러려면 아직 구현하지 못한 function-references, exception-handling, gc 세 제안이 필요합니다.

마지막으로 표현 하나를 짚습니다. 긱뉴스 토픽 제목인 "가장 빠른 Wasm 인터프리터를 만드는 기술"은 발표문 제목(Wasmi 2.0 - Engineering of the Fastest Wasm Interpreters)을 옮긴 것이고, 발표문 결론도 2.0을 "the category of the fastest portable Wasm interpreters"에 둡니다. 이 글이 수준을 '경쟁 구현과 대등하고 때로는 능가한다'(릴리스 노트: on par and sometimes even exceeds)에 두는 것은 그 쪽 근거를 따른 것이고 그 이상의 최상급은 쓰지 않았습니다.

근거와 참고

이 글 공유하기xXfacebook페이스북