홈으로 돌아가기

파서 3배 가속: Rust/WASM에서 TypeScript으로 전환

기사가 DSL 파서를 Rust/WASM에서 TypeScript으로 재작성한 사례를 설명하며, 처리 속도가 2.6–3.3배 가속된 이유를 분석합니다. WASM-JS 경계 오버헤드 원인과 지시어 캐싱을 통한 증분 알고리즘의 장점을 분석합니다.

WASM을 포기해 파서 속도를 3배 높인 방법
Advertisement 728x90

# WASM 포기: Rust 파서를 TypeScript로 재작성해 속도 3배 향상

브라우저에서 커스텀 DSL 파싱을 위해 Rust/WASM에서 TypeScript로 전환하면 런타임 경계 오버헤드가 사라지고 증분 알고리즘이 가능해져 전체 스트림 처리 시간이 2.6~3.3배 단축됩니다.

openui-lang 파서 아키텍처

이 파서는 언어 모델(LLM)이 생성한 커스텀 DSL을 React 컴포넌트 트리로 변환합니다. 파이프라인은 6단계 순차 처리로 구성되며, 각 단계가 특정 작업을 담당합니다:

  • 자동 마감자(Autocloser) — 최소한의 닫기 괄호와 따옴표를 추가해 부분 텍스트의 문법적 올바로움을 보장합니다.
  • 렉서(Lexer) — 입력 문자열을 한 번 스캔해 타입화된 토큰을 생성합니다.
  • 분할기(Splitter) — 토큰 스트림을 개별 id = expression 명령어로 나눕니다.
  • 파서(Parser) — 재귀 하강 방식으로 표현식에서 추상 구문 트리(AST)를 구축합니다.
  • 해석기(Resolver) — 변수 참조를 해결하고 선언 호이스팅을 지원하며 순환 종속성을 감지합니다.
  • 매퍼(Mapper) — 내부 AST를 React 렌더링 준비된 공개 OutputNode 형식으로 변환합니다.

각 단계는 LLM 출력 스트림의 모든 청크에서 실행되므로 전체 성능에 핵심적입니다.

Google AdInline article slot

문제: WASM–JS 경계 오버헤드가 지배적

원래 Rust 구현(WebAssembly로 컴파일)은 파싱 자체가 느린 게 아니라 런타임 간 데이터 전송 비용에 발목이 잡혔습니다. 매 호출마다:

  • JS 힙에서 WASM 선형 메모리로 문자열 복사.
  • Rust 내에서 파싱 결과를 JSON으로 직렬화.
  • JSON 문자열을 JS 힙으로 복사.
  • JSON.parse()로 문자열을 JS 객체로 역직렬화.

JSON 직렬화 없이 직접 객체 전송을 위한 serde-wasm-bindgen 시도는 오히려 성능을 9~29% 악화시켰습니다. Rust 데이터 구조를 재귀적으로 네이티브 JS 객체로 변환하며 미세한 런타임 횡단이 폭발적으로 발생했기 때문입니다.

해결책: 완전 TypeScript 이식 + 증분 알고리즘

WASM을 완전히 버리고 TypeScript로 전환해 데이터 전송 오버헤드를 없앴습니다. 하지만 진짜 획기적 변화는 스트리밍 최적화를 위한 증분 접근법이었습니다.

Google AdInline article slot

단순 버전은 새 청크마다 누적된 전체 문자열을 처음부터 재파싱해 청크 수에 대한 2차 O(N²) 복잡도를 초래했습니다.

명령어 수준 캐싱을 도입한 증분 전략:

  • 중첩 수준 0에서 개행으로 끝나는 명령어는 불변으로 간주.
  • 첫 파싱 후 AST를 캐싱.
  • 새 청크에서 꼬리(미완성) 명령어만 재파싱.
  • 최종 결과는 캐싱된 AST와 새 꼬리 파싱을 병합.

이로 복잡도가 전체 문서 길이 L에 대한 선형 O(L)로 줄었습니다.

Google AdInline article slot

벤치마크 및 성능 결과

세 가지 실제 LLM 생성 픽스처로 테스트했습니다.

단일 파서 호출 (중간 시간, 마이크로초):

| 픽스처 | TypeScript | WASM (JSON 경유) | 속도 향상 |

|--------------|------------|------------------|-----------|

| simple-table | 9.3 | 20.5 | 2.2배 |

| contact-form | 13.4 | 61.4 | 4.6배 |

| dashboard | 19.4 | 57.9 | 3.0배 |

전체 스트림 처리 비용 (중간 시간, 마이크로초):

| 픽스처 | 단순 TS (O(N²)) | 증분 TS (O(N)) | 속도 향상 |

|--------------|------------------|----------------|-----------|

| simple-table | 69 | 77 | — |

| contact-form | 316 | 122 | 2.6배 |

| dashboard | 840 | 255 | 3.3배 |

simple-table은 캐싱할 게 없는 단일 명령어여서 향상이 없었습니다. 명령어 수가 많아질수록 이득이 커집니다.

주요 교훈

  • 빈번한 호출이 있는 Rust/WASM 설정에서 진짜 살인자는 컴퓨트가 아니라 경계 간 데이터 복사와 직렬화입니다.
  • serde-wasm-bindgen을 통한 직접 WASM-to-JS 객체 전달은 내부 변환으로 인해 조정된 JSON보다 느릴 수 있습니다.
  • 알고리즘 개선(O(N²) → O(N))이 언어나 런타임 미세 최적화보다 종종 우월합니다.
  • WASM은 상호작용 적은 작업(이미지 처리)에 빛나지만, 복잡한 JS 객체로의 빈번한 파싱에는 부적합합니다.

WASM 사용할 때 (그리고 안 할 때)

WASM 승리:

  • 대량 입력과 스칼라/현장 출력이 있는 컴퓨트 집약 작업(미디어 처리, 암호화, 물리 시뮬레이션).
  • 전체 재작성 없이 기존 C/C++ 라이브러리를 브라우저로 이식.

WASM 패배:

  • 복잡한 JS 객체 반환을 요하는 구조화 텍스트의 빈번한 파싱.
  • 경계 오버헤드가 상쇄되지 않는 작은 입력의 핫 패스.

— Editorial Team

Advertisement 728x90

다음 읽기