Zpět na domů

Zrychlení parseru 3násobně: přechod z Rust/WASM na TypeScript

Článek popisuje případ přepsání parseru DSL z Rust/WASM na TypeScript, což vedlo k zrychlení zpracování 2,6–3,3násobně. Analyzují se příčiny overheadu na hranici WASM-JS a výhody inkrementálního algoritmu s cachováním instrukcí.

Jak jsme zrychlili parser 3násobně, opustivši WASM
Advertisement 728x90

Vzdání se WASM: Jak přepsání parseru z Rustu na TypeScript zrychlilo zpracování 3×

Přechod z Rust/WASM na TypeScript pro parsování DSL v prohlížeči odstranil režii na hranici prostředí a umožnil implementovat inkrementální algoritmus, čímž se celkový čas zpracování toku snížil 2,6–3,3krát.

Architektura parseru openui-lang

Parser transformuje vlastní DSL, generovaný jazykovým modelem (LLM), na strom React komponent. Konvejor se skládá ze šesti po sobě jdoucích kroků, z nichž každý plní specifický úkol:

  • Autocloser – zajišťuje syntaktickou správnost dílčího textu přidáním minimálního počtu uzavíracích závorek a uvozovek.
  • Lexer – provádí jednoprůchodové skenování vstupního řetězce a generuje typizované tokeny.
  • Splitter – dělí tok tokenů na samostatné instrukce formátu id = výraz.
  • Parser – využívá rekurzivní sestup k vytvoření abstraktního syntaktického stromu (AST) z výrazů.
  • Resolver – rozpoznává reference na proměnné, podporuje hoisting deklarací a odhaluje cyklické závislosti.
  • Mapper – transformuje interní AST na veřejný formát OutputNode, připravený pro React renderer.

Každý krok je klíčový pro konečný výkon, protože parser je volán na každém fragmentu výstupního toku LLM.

Google AdInline article slot

Problém: Dominující režie na hranici WASM–JS

Původní implementace v Rustu, zkompilovaná do WebAssembly, netrpěla pomalým parsováním, ale náklady na přenos dat mezi prostředími. Každé volání zahrnovalo:

  • Kopírování řetězce z haldy JS do lineární paměti WASM.
  • Serializaci výsledku parsování do JSON uvnitř Rustu.
  • Kopírování JSON řetězce zpět do haldy JS.
  • Deserializaci řetězce na JS objekt pomocí JSON.parse().

Pokus použít serde-wasm-bindgen pro přímý přenos objektů bez serializace do JSON zhoršil výkon o 9–29 %. To bylo způsobeno nutností rekurzivně materializovat datové struktury Rustu na nativní JS objekty, což vytvářelo mnoho jemnozrnných přechodů přes hranici prostředí.

Řešení: Úplný přenos na TypeScript a inkrementální algoritmus

Úplné opuštění WASM a přechod na TypeScript odstranil režii na přenosu dat. Klíčovým vylepšením však byla optimalizace algoritmu pro streamové zpracování.

Google AdInline article slot

Naivní implementace volala parser na každém novém fragmentu, pokaždé zpracovávajíc celý nahromaděný řetězec od začátku. To vedlo ke kvadratické složitosti O(N²) vzhledem k počtu fragmentů.

Byla zavedena inkrementální strategie s cachováním na úrovni instrukcí:

  • Instrukce dokončené odřádkováním na nulové úrovni vnoření jsou považovány za neměnné.
  • AST takových dokončených instrukcí je cachováno po prvním parsování.
  • Při příchodu nového fragmentu je znovu parsován pouze ocasní, nedokončená instrukce.
  • Výsledek je vytvořen spojením cachovaných AST a čerstvého výsledku parsování ocasu.

Toto snížilo složitost na lineární O(L), kde L je celková délka dokumentu.

Google AdInline article slot

Benchmarky a výsledky výkonu

Srovnání bylo provedeno na třech realistických fixturech, generovaných LLM.

Jednotlivé volání parseru (medián času, mikrosekundy):

| Fixture | TypeScript | WASM (přes JSON) | Zrychlení |

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

| jednoduchá-tabulka | 9,3 | 20,5 | 2,2× |

| kontaktní-formulář | 13,4 | 61,4 | 4,6× |

| dashboard | 19,4 | 57,9 | 3,0× |

Celková cena zpracování toku (medián času, mikrosekundy):

| Fixture | Naivní TS (O(N²)) | Inkrementální TS (O(N)) | Zrychlení |

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

| jednoduchá-tabulka | 69 | 77 | — |

| kontaktní-formulář | 316 | 122 | 2,6× |

| dashboard | 840 | 255 | 3,3× |

Pro jednoduchou-tabulku není zrychlení, protože dokument sestává z jedné instrukce a není co cachovat. Výhoda roste s rostoucím počtem instrukcí v dokumentu.

Co je důležité

  • Hlavním úzkým místem v kombinaci Rust/WASM pro častá volání nejsou výpočty, ale režie na kopírování a serializaci dat přes hranici prostředí.
  • Přímý přenos objektů z WASM do JS přes serde-wasm-bindgen je často pomalejší než optimalizovaná serializace do JSON kvůli mnoha interním transformacím.
  • Optimalizace algoritmické složitosti (přechod z O(N²) na O(N)) může přinést větší praktický užitek než mikrooptimalizace na úrovni jazyka nebo běhového prostředí.
  • WASM je efektivní pro úlohy s minimální interakcí (např. zpracování obrázků), ale není vhodný pro časté parsování strukturovaného textu do JS objektů.

Kdy používat WASM a kdy ne

Efektivní scénáře pro WASM:

  • Výpočetně náročné úlohy s velkým objemem vstupních dat a skalárním nebo in-place výstupem (zpracování médií, kryptografie, fyzické simulace).
  • Portování existujících nativních knihoven (C/C++) do prohlížeče bez úplného přepsání.

Neeffektivní scénáře pro WASM:

  • Časté parsování strukturovaného textu s nutností vracet složité JS objekty.
  • Funkce volané často s malými vstupními daty, kde se režie na přechodu neamortizuje.

— Editorial Team

Advertisement 728x90

Číst dál