-
Rust로 작성된 WASM 파서는 구조적으로 빠르지만, JS-WASM 경계에서의 데이터 복사와 직렬화 오버헤드가 성능 병목으로 드러남
-
serde-wasm-bindgen을 통한 직접 객체 반환은 JSON 직렬화보다 9~29% 느렸으며, 이는 런타임 간 세밀한 변환 비용 때문임
-
TypeScript로 전체 파이프라인을 포팅하자 동일한 아키텍처에서 2.2~4.6배 빠른 단일 호출 성능을 달성
- 스트리밍 처리에서는 문 단위 캐싱을 통한 O(N²)→O(N) 개선으로 2.6~3.3배 빠른 전체 처리 속도 확보
- 결과적으로, WASM은 계산 집약적·저빈도 호출에 적합하고, JS 객체 파싱이나 잦은 호출 함수에는 부적합함이 확인됨
Rust WASM 파서의 구조와 한계
-
openui-lang 파서는 LLM이 생성한 DSL을 React 컴포넌트 트리로 변환하는 6단계 파이프라인으로 구성
- 단계: autocloser → lexer → splitter → parser → resolver → mapper → ParseResult
- 각 단계는 토큰화, 구문 분석, 변수 해석, AST 변환 등을 수행
- Rust 코드 자체는 빠르지만, JS↔WASM 간 문자열 복사·JSON 직렬화·역직렬화 과정이 매 호출마다 발생
- 입력 문자열 복사(JS→WASM), Rust 내부 파싱, 결과 JSON 직렬화, JSON 복사(WASM→JS), JS에서 역직렬화
- 이 경계 오버헤드가 전체 성능을 지배했으며, Rust 계산 속도는 병목이 아님
serde-wasm-bindgen 시도와 실패
- JSON 직렬화를 피하기 위해 Rust 구조체를 직접 JS 객체로 반환하는 serde-wasm-bindgen을 적용
- 그러나 30% 느려짐이 관찰됨
- Rust 구조체 메모리를 JS가 직접 읽을 수 없고, 런타임 간 메모리 레이아웃이 달라 필드 단위 변환이 필요
- 반면 JSON 직렬화는 Rust 내부에서 한 번의 문자열 생성 후, JS에서 최적화된 JSON.parse로 처리
- 벤치마크 결과
Fixture
JSON round-trip
serde-wasm-bindgen
변화
| simple-table |
20.5µs |
22.5µs |
-9% |
| contact-form |
61.4µs |
79.4µs |
-29% |
| dashboard |
57.9µs |
74.0µs |
-28% |
TypeScript로의 전환과 성능 향상
- 동일한 6단계 구조를 TypeScript로 완전 포팅, WASM 경계를 제거하고 V8 힙 내에서 직접 실행
- 단일 호출 기준 벤치마크 결과
Fixture
TypeScript
WASM
속도 향상
| simple-table |
9.3µs |
20.5µs |
2.2배 |
| contact-form |
13.4µs |
61.4µs |
4.6배 |
| dashboard |
19.4µs |
57.9µs |
3.0배 |
-
WASM 제거만으로 호출당 비용이 대폭 감소, 그러나 스트리밍 구조의 비효율은 여전히 존재
스트리밍 파싱의 O(N²) 문제와 개선
- LLM 출력이 여러 청크로 전달될 때, 매번 전체 누적 문자열을 재파싱하는 O(N²) 비효율 발생
- 예: 1000자 문서를 20자씩 50회 파싱 → 총 25,000자 처리
- 해결책으로 문 단위 증분 캐싱(incremental caching) 도입
- 완성된 문장은 캐시하고, 진행 중인 문장만 재파싱
- 캐시된 AST와 새 AST를 병합해 결과 반환
- 전체 스트림 기준 벤치마크
Fixture
나이브 TS
증분 TS
속도 향상
| simple-table |
69µs |
77µs |
없음 |
| contact-form |
316µs |
122µs |
2.6배 |
| dashboard |
840µs |
255µs |
3.3배 |
- 문장이 많을수록 캐시 효과가 커지고, 전체 처리량이 선형적으로 개선
WASM 사용에 대한 교훈
-
적합한 경우
-
계산 집약적·상호작용 적은 작업: 이미지·비디오 처리, 암호화, 물리 시뮬레이션, 오디오 코덱 등
-
기존 네이티브 라이브러리 이식: SQLite, OpenCV, libpng 등
-
부적합한 경우
-
JS 객체로 구조화된 텍스트 파싱: 직렬화 비용이 지배적
-
짧은 입력을 자주 호출하는 함수: 경계 비용이 계산보다 큼
- 핵심 교훈
-
병목 위치를 프로파일링 후 언어를 선택해야 함
-
serde-wasm-bindgen의 직접 객체 전달은 더 비쌈
-
알고리듬 복잡도 개선이 언어 전환보다 효과적
-
WASM과 JS는 힙을 공유하지 않으며, 변환 비용은 항상 존재
최종 결과: TypeScript 전환과 증분 캐싱으로 호출당 2.2~4.6배, 전체 스트림 2.6~3.3배 성능 향상 달성