Vai al contenuto

Perché Abbiamo Scelto Rust e WebAssembly per Strumenti di Calcolo Ingegneristico

Una analisi tecnica del motivo per cui il motore di calcolo di ChainSolve è scritto in Rust e compilato a WebAssembly, e cosa ciò significa per prestazioni, affidabilità e portabilità.

BG ben godfrey · · 6 min read
RUST

I Requisiti

Quando abbiamo iniziato a costruire il motore di calcolo di ChainSolve, avevamo una serie chiara di requisiti:

  1. Velocità di calcolo prossima al nativo. I calcoli ingegneristici possono comportare matrici di grandi dimensioni, risolutori iterativi e grafi di dipendenza complessi con centinaia di nodi. Il motore deve valutare questi in millisecondi, non in secondi.

  2. Esecuzione nel browser. Lo strumento deve funzionare interamente nel browser senza round-trip verso il server per il calcolo. Gli ingegneri che lavorano su progetti sensibili non possono inviare dati di calcolo proprietari a server esterni.

  3. Capacità offline. Gli ingegneri negli impianti di prova, sui pavimenti di fabbrica e presso i siti dei clienti spesso hanno connettività internet limitata o assente. Lo strumento deve funzionare offline una volta caricato.

  4. Sicurezza della memoria. Uno strumento di calcolo che si arresta o produce risultati silenziosamente errati a causa di corruzione della memoria è peggio che inutile. La correttezza è non negoziabile.

  5. Portabilità. Lo stesso motore deve funzionare nei browser, in Node.js per integrazione CI/CD, e potenzialmente come strumento CLI nativo.

Questi requisiti hanno ristretto considerevolmente le opzioni.

Perché Non JavaScript?

JavaScript è la scelta ovvia per le applicazioni basate su browser. È il linguaggio nativo del web, ha eccellenti strumenti e un ecosistema massiccio. Per il codice UI, utilizziamo JavaScript (specificamente TypeScript con React). Ma per il motore di calcolo, JavaScript ha limitazioni fondamentali.

JavaScript è un linguaggio dinamicamente tipizzato e con raccolta dei rifiuti. Per il rendering UI e la gestione degli eventi, va bene. Per il calcolo numerico, introduce due problemi:

Limite di prestazioni. I moderni motori JavaScript come V8 sono sorprendentemente veloci per un linguaggio dinamico, ma non possono eguagliare le prestazioni del codice compilato ahead-of-time per il lavoro numericamente intensivo. La compilazione JIT aggiunge pause impredittibili. La raccolta dei rifiuti aggiunge picchi di latenza. Questi sono accettabili in un livello UI ma problematici in un motore di calcolo che necessita di una valutazione coerente e veloce.

Precisione numerica. JavaScript ha un singolo tipo di numero: virgola mobile a precisione doppia IEEE 754. Questo è adeguato per la maggior parte dei calcoli ingegneristici, ma JavaScript non fornisce alcun modo per controllare il comportamento della virgola mobile, nessuna istruzione SIMD per il calcolo vettorializzato, e nessun tipo intero per i casi in cui è richiesta l’aritmetica intera esatta (come gli algoritmi su grafi del grafo di dipendenza).

Un motore di calcolo JavaScript funzionerebbe. Sarebbe adeguato per calcoli piccoli e medi. Ma raggiungerebbe un limite di prestazioni con catene complesse e richiederebbe workaround attenti per i casi limite numerici.

Perché Rust?

Rust soddisfa ogni uno dei nostri requisiti di calcolo:

Prestazioni. Rust compila a codice macchina (o WebAssembly) senza raccoglitore di rifiuti e senza sovraccarico di runtime. Il codice numerico in Rust ha prestazioni comparabili a C e C++. Per il motore di calcolo di ChainSolve, ciò significa che la valutazione della catena è coerentemente veloce, senza pause GC o warmup JIT.

Sicurezza della memoria senza raccolta dei rifiuti. Il sistema di proprietà di Rust garantisce la sicurezza della memoria al momento della compilazione. Non ci sono dereference di puntatori nulli, nessun bug di use-after-free, nessuna corsa ai dati. Per uno strumento di calcolo dove la correttezza è fondamentale, questo è un vantaggio significativo rispetto a C e C++.

Obiettivo WebAssembly. Rust ha supporto di prima classe per la compilazione a WebAssembly tramite wasm-pack e wasm-bindgen. Il modulo WASM risultante viene eseguito in qualsiasi browser moderno a velocità quasi nativa. Questo ci offre l’esecuzione nel browser e la capacità offline senza sacrificare le prestazioni.

Sistema di tipo forte. Il sistema di tipo di Rust ci permette di codificare i vincoli ingegneristici nel sistema di tipo stesso. Un UnitValue<Millimeters> non può essere accidentalmente aggiunto a un UnitValue<Inches>, il compilatore lo rifiuta. Questo cattura intere categorie di errori ingegneristici al momento della compilazione.

Portabilità. Lo stesso codice Rust compila a WASM per l’esecuzione nel browser, a binari nativi per gli strumenti CLI, e a librerie condivise per l’integrazione con altri strumenti. Un’unica codebase, più obiettivi.

L’Architettura

L’architettura di ChainSolve separa il calcolo dalla presentazione:

+------------------------------------------+
|  Browser (TypeScript + React)            |
|  - UI components                          |
|  - Chain editor                           |
|  - Visualisation                          |
+------------------------------------------+
          |  wasm-bindgen interface  |
+------------------------------------------+
|  WASM Module (Rust)                      |
|  - Dependency graph solver               |
|  - Block evaluation engine               |
|  - Unit conversion system                |
|  - Expression parser                     |
|  - Numerical methods library             |
+------------------------------------------+

Il modulo Rust/WASM espone un’API pulita al livello TypeScript tramite wasm-bindgen. Il livello TypeScript gestisce tutte le considerazioni UI, il rendering, l’interazione dell’utente, il layout. Il livello Rust gestisce tutti i calcoli, l’attraversamento dei grafi, la valutazione dei blocchi, la conversione delle unità, l’analisi delle espressioni.

Questa separazione significa che l’UI non blocca mai il calcolo. La valutazione della catena avviene nel modulo WASM, restituisce i risultati a TypeScript e l’UI si aggiorna. Per catene molto grandi, eseguiamo il modulo WASM in un Web Worker per mantenere il thread UI completamente responsivo.

Risultati delle Prestazioni

Alcuni benchmark rappresentativi dalle nostre build di sviluppo (misurati su un laptop di fascia media, M2 MacBook Air):

OperazioneJavaScript (baseline)Rust/WASM
Valutare catena di 100 blocchi12 ms0,8 ms
Valutare catena di 1.000 blocchi340 ms8 ms
Ordinamento topologico (1.000 nodi)5 ms0,3 ms
Conversione unitaria (10.000 valori)18 ms0,6 ms
Analisi + valutazione espressione0,4 ms0,02 ms

L’implementazione Rust/WASM è coerentemente da 15 a 40 volte più veloce dell’equivalente JavaScript. Per catene piccole, entrambe sono abbastanza veloci. Per catene grandi (che gli utenti di produzione inevitabilmente creeranno), la differenza è tra feedback istantaneo e ritardo percettibile.

Sfide e Compromessi

Rust e WebAssembly non sono senza compromessi:

Curva di apprendimento. Rust è un linguaggio più complesso di JavaScript o Python. Il sistema di proprietà, sebbene potente, richiede un modello mentale che richiede tempo per svilupparsi. Per un fondatore singolo, questo era un investimento nella produttività a lungo termine al costo della velocità a breve termine.

Dimensione del binario WASM. Il modulo WASM compilato è attualmente di circa 800 KB gzipped. Questo è un’aggiunta significativa al carico della pagina iniziale. Lo attenuiamo con il caricamento lento, il modulo WASM viene caricato in modo asincrono dopo il rendering della shell dell’interfaccia utente.

Debug. Il debug del codice Rust compilato a WASM sta migliorando ma è ancora meno conveniente del debug di JavaScript in browser DevTools. Ci affidiamo fortemente alla suite di test di Rust (eseguita in modo nativo) e utilizziamo il logging specifico di WASM per il debug del browser.

Ecosistema. L’ecosistema Rust per il calcolo numerico sta crescendo ma è meno maturo dell’ecosistema NumPy/SciPy di Python. Abbiamo implementato alcuni metodi numerici da zero dove i crate Rust esistenti erano insufficienti.

Nonostante questi compromessi, la decisione di utilizzare Rust e WebAssembly è stata convalidata dalle caratteristiche di prestazioni e affidabilità del motore risultante. Per uno strumento su cui gli ingegneri faranno affidamento sui risultati per decisioni critiche per la sicurezza, le garanzie al momento della compilazione che Rust fornisce valgono la complessità di sviluppo aggiuntiva.

Conclusione

La scelta di Rust e WebAssembly per il motore di calcolo di ChainSolve è stata una decisione architetturale deliberata guidata dai requisiti ingegneristici, non dalla moda tecnologica. Prestazioni quasi native nel browser, sicurezza della memoria senza raccolta dei rifiuti, e un sistema di tipo forte che cattura gli errori di unità al momento della compilazione, questi non sono vantaggi secondari per uno strumento di calcolo ingegneristico. Sono requisiti.

Se sei interessato ai dettagli tecnici della nostra architettura Rust/WASM, o se stai valutando scelte tecnologiche simili per i tuoi strumenti ingegneristici, accogliamo la conversazione. Contattaci tramite la nostra pagina di contatto.

Written by
BG
ben godfrey
Ingegnere presso Godfrey Engineering Ltd.