- Le parseur WASM écrit en Rust est structurellement rapide, mais la copie des données et le surcoût de sérialisation à la frontière JS-WASM se sont révélés être le goulot d’étranglement des performances
- Le retour direct d’objets via
serde-wasm-bindgens’est avéré 9 à 29 % plus lent que la sérialisation JSON, en raison du coût des conversions fines entre runtimes - En portant tout le pipeline en TypeScript, les mêmes choix d’architecture ont permis d’obtenir des performances par appel 2,2 à 4,6 fois supérieures
- En traitement de flux, le passage de O(N²) à O(N) grâce à un cache incrémental par phrase a permis d’obtenir une vitesse de traitement globale 2,6 à 3,3 fois supérieure
- En conséquence, il apparaît que WASM convient aux traitements intensifs en calcul et peu fréquents, mais pas au parsing d’objets JS ni aux fonctions appelées très souvent
Structure et limites du parseur Rust WASM
- Le parseur
openui-langest constitué d’un pipeline en 6 étapes qui convertit un DSL généré par un LLM en arbre de composants React- Étapes :
autocloser → lexer → splitter → parser → resolver → mapper → ParseResult - Chaque étape effectue la tokenisation, l’analyse syntaxique, la résolution des variables, la transformation d’AST, etc.
- Étapes :
- Le code Rust lui-même est rapide, mais les opérations de copie de chaînes, sérialisation JSON et désérialisation entre JS↔WASM se produisent à chaque appel
- Copie de la chaîne d’entrée (JS→WASM), parsing côté Rust, sérialisation du résultat en JSON, copie du JSON (WASM→JS), puis désérialisation côté JS
- Ce surcoût à la frontière dominait les performances globales, la vitesse de calcul de Rust n’étant pas le goulot d’étranglement
Tentative avec serde-wasm-bindgen et échec
- Pour éviter la sérialisation JSON,
serde-wasm-bindgen, qui retourne directement des structures Rust sous forme d’objets JS, a été appliqué - Mais on a observé une baisse de performance de 30 %
- JS ne peut pas lire directement la mémoire des structures Rust, et comme la disposition mémoire diffère entre runtimes, une conversion champ par champ est nécessaire
- À l’inverse, la sérialisation JSON consiste à générer une chaîne une seule fois côté Rust, puis à la traiter côté JS via un
JSON.parseoptimisé
- Résultats du benchmark
Fixture Aller-retour JSON serde-wasm-bindgen Variation 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%
Passage à TypeScript et amélioration des performances
- Le même pipeline en 6 étapes a été entièrement porté en TypeScript, supprimant la frontière WASM pour s’exécuter directement dans le heap de V8
- Résultats du benchmark par appel
Fixture TypeScript WASM Gain de vitesse simple-table 9.3µs 20.5µs 2.2x contact-form 13.4µs 61.4µs 4.6x dashboard 19.4µs 57.9µs 3.0x - La simple suppression de WASM a fortement réduit le coût par appel, mais l’inefficacité de la structure de streaming restait présente
Le problème en O(N²) du parsing en flux et son amélioration
- Lorsque la sortie du LLM est livrée en plusieurs chunks, le fait de reparser à chaque fois toute la chaîne accumulée introduit une inefficacité en O(N²)
- Exemple : parser un document de 1000 caractères 50 fois par blocs de 20 caractères revient à traiter 25 000 caractères au total
- La solution a consisté à introduire un cache incrémental par phrase
- Les phrases complètes sont mises en cache, et seule la phrase en cours est reparsée
- L’AST en cache est fusionné avec le nouvel AST avant de retourner le résultat
- Benchmarks sur l’ensemble du flux
Fixture TS naïf TS incrémental Gain de vitesse simple-table 69µs 77µs Aucun contact-form 316µs 122µs 2.6x dashboard 840µs 255µs 3.3x - Plus il y a de phrases, plus l’effet du cache augmente, et le débit global s’améliore linéairement
Enseignements sur l’usage de WASM
- Cas adaptés
- Travaux intensifs en calcul avec peu d’interactions : traitement d’image et de vidéo, chiffrement, simulation physique, codecs audio, etc.
- Portage de bibliothèques natives existantes : SQLite, OpenCV, libpng, etc.
- Cas inadaptés
- Parsing de texte structuré en objets JS : le coût de sérialisation devient dominant
- Fonctions appelées fréquemment sur des entrées courtes : le coût de frontière dépasse celui du calcul
- Principaux enseignements
- Il faut profiler les goulots d’étranglement avant de choisir le langage
- Le passage direct d’objets avec
serde-wasm-bindgencoûte plus cher - Améliorer la complexité algorithmique est plus efficace qu’un changement de langage
- WASM et JS ne partagent pas le même heap, et le coût de conversion existe toujours
Résultat final : le passage à TypeScript et le cache incrémental ont permis d’obtenir des performances 2,2 à 4,6 fois supérieures par appel, et 2,6 à 3,3 fois supérieures sur l’ensemble du flux
Aucun commentaire pour le moment.