paint-brush
Perché il benchmarking JavaScript è un pasticcio?di@asyncbanana
Nuova storia

Perché il benchmarking JavaScript è un pasticcio?

di AsyncBanana7m2025/01/04
Read on Terminal Reader

Troppo lungo; Leggere

Il benchmarking di JavaScript è ancora necessario, soprattutto perché JavaScript è utilizzato in applicazioni più sensibili alle prestazioni. I motori JavaScript hanno fatto del loro meglio per mitigare gli attacchi di temporizzazione, insieme a un passaggio a [Do Not Track] I motori JavaScript rendono intenzionalmente imprecise le misurazioni di temporizzazione.
featured image - Perché il benchmarking JavaScript è un pasticcio?
AsyncBanana HackerNoon profile picture
0-item
1-item

Odio il benchmarking del codice, proprio come qualsiasi essere umano (e, a questo punto, la maggior parte degli spettatori di questo probabilmente non lo è ¯\ (ツ) /¯). È molto più divertente fingere che la memorizzazione nella cache di un valore abbia aumentato le prestazioni del 1000% piuttosto che testare per vedere cosa ha fatto. Ahimè, il benchmarking in JavaScript è ancora necessario, soprattutto perché JavaScript è usato ( quando non dovrebbe? ) in applicazioni più sensibili alle prestazioni. Sfortunatamente, a causa di molte delle sue decisioni architettoniche fondamentali, JavaScript non rende il benchmarking più semplice.

Cosa c'è che non va in JavaScript?

Il compilatore JIT diminuisce la precisione(?)

Per chi non ha familiarità con la magia dei moderni linguaggi di scripting come JavaScript, la loro architettura può essere piuttosto complessa. Invece di eseguire solo codice tramite un interprete che sputa immediatamente istruzioni, la maggior parte dei motori JavaScript utilizza un'architettura più simile a un linguaggio compilato come C: integrano più livelli di "compilatori" .


Ognuno di questi compilatori offre un diverso compromesso tra tempo di compilazione e prestazioni di runtime, quindi l'utente non deve spendere tempo per ottimizzare il codice di elaborazione che viene eseguito raramente, sfruttando al contempo i vantaggi prestazionali del compilatore più avanzato per il codice che viene eseguito più spesso (i "percorsi caldi"). Ci sono anche altre complicazioni che sorgono quando si utilizzano compilatori di ottimizzazione che coinvolgono parole di programmazione elaborate come " monomorfismo di funzione ", ma vi risparmierò ed eviterò di parlarne qui.


Quindi... perché questo è importante per il benchmarking? Beh, come avrai intuito, poiché il benchmarking misura le prestazioni del codice, il compilatore JIT può avere un'influenza piuttosto grande. Pezzi di codice più piccoli, quando sottoposti a benchmarking, possono spesso vedere miglioramenti delle prestazioni di 10 volte+ dopo l'ottimizzazione completa, introducendo molti errori nei risultati.


Ad esempio, nella configurazione di benchmarking più elementare (per diversi motivi non utilizzare nulla di simile a quanto riportato di seguito):

 for (int i = 0; i<1000; i++) { console.time() // do some expensive work console.timeEnd() }

(Non preoccupatevi, parleremo anche di console.time )


Gran parte del tuo codice verrà memorizzato nella cache dopo alcune prove, riducendo significativamente il tempo per operazione. I programmi di benchmark spesso fanno del loro meglio per eliminare questa memorizzazione nella cache/ottimizzazione, poiché può anche far apparire i programmi testati più avanti nel processo di benchmark relativamente più veloci. Tuttavia, devi in ultima analisi chiederti se i benchmark senza ottimizzazioni corrispondono alle prestazioni nel mondo reale.


Certo, in certi casi, come le pagine web a cui si accede raramente, l'ottimizzazione è improbabile, ma in ambienti come i server, dove le prestazioni sono più importanti, l'ottimizzazione dovrebbe essere prevista. Se si esegue un pezzo di codice come middleware per migliaia di richieste al secondo, è meglio sperare che V8 lo stia ottimizzando.


Quindi, in pratica, anche all'interno di un motore, ci sono 2-4 modi diversi per eseguire il codice con diversi livelli di prestazioni. Oh, inoltre, è incredibilmente difficile in certi casi garantire che determinati livelli di ottimizzazione siano abilitati. Divertiti :).

I motori fanno del loro meglio per impedirti di cronometrare con precisione

Conoscete il fingerprinting? La tecnica che ha permesso di usare Do Not Track per facilitare il tracciamento ? Sì, i motori JavaScript hanno fatto del loro meglio per mitigarlo. Questo sforzo, insieme a una mossa per prevenire gli attacchi di timing , ha portato i motori JavaScript a rendere intenzionalmente il timing impreciso, così gli hacker non possono ottenere misurazioni precise delle prestazioni attuali dei computer o di quanto sia costosa una certa operazione.


Sfortunatamente, questo significa che, se non si apportano modifiche, i benchmark presentano lo stesso problema.


L'esempio nella sezione precedente sarà impreciso, poiché misura solo in millisecondi. Ora, sostituiscilo con performance.now() . Ottimo.


Ora abbiamo timestamp in microsecondi!

 // Bad console.time(); // work console.timeEnd(); // Better? const t = performance.now(); // work console.log(performance.now() - t);

Tranne... sono tutti in incrementi di 100μs. Ora, aggiungiamo alcune intestazioni per mitigare il rischio di attacchi di temporizzazione. Oops, possiamo ancora solo incrementi di 5μs. 5μs è probabilmente una precisione sufficiente per molti casi d'uso, ma dovrai cercare altrove qualsiasi cosa che richieda più granularità. Per quanto ne so, nessun browser consente timer più granulari. Node.js lo fa, ma ovviamente, questo ha i suoi problemi.


Anche se decidi di eseguire il tuo codice tramite il browser e lasciare che il compilatore faccia il suo lavoro, chiaramente avrai comunque più grattacapi se vuoi tempi precisi. Oh sì, e non tutti i browser sono uguali.

Ogni ambiente è diverso

Amo Bun per quello che ha fatto per far progredire JavaScript lato server, ma accidenti, rende molto più difficile il benchmarking di JavaScript per i server. Qualche anno fa, gli unici ambienti JavaScript lato server che interessavano alle persone erano Node.js e Deno , entrambi i quali utilizzavano il motore JavaScript V8 (lo stesso in Chrome). Bun invece utilizza JavaScriptCore, il motore in Safari, che ha caratteristiche di prestazioni completamente diverse.


Questo problema di più ambienti JavaScript con le proprie caratteristiche di performance è relativamente nuovo nel JavaScript lato server, ma affligge i client da molto tempo. I 3 diversi motori JavaScript comunemente usati, V8, JSC e SpiderMonkey per Chrome, Safari e Firefox, rispettivamente, possono tutti funzionare in modo significativamente più veloce o più lento su un pezzo di codice equivalente.


Un esempio di queste differenze è Tail Call Optimization (TCO). TCO ottimizza le funzioni che ricorrono alla fine del loro corpo, in questo modo:

 function factorial(i, num = 1) { if (i == 1) return num; num *= i; i--; return factorial(i, num); }


Prova a fare il benchmarking di factorial(100000) in Bun. Ora, prova la stessa cosa in Node.js o Deno. Dovresti ottenere un errore simile a questo:

 function factorial(i, num = 1) { ^ RangeError: Maximum call stack size exceeded


In V8 (e per estensione Node.js e Deno), ogni volta che factorial() richiama se stesso alla fine, il motore crea un contesto di funzione completamente nuovo in cui eseguire la funzione annidata, che alla fine è limitato dallo stack di chiamate. Ma perché questo non accade in Bun? JavaScriptCore, che Bun usa, implementa TCO, che ottimizza questi tipi di funzioni trasformandole in un ciclo for più simile a questo:

 function factorial(i, num = 1) { while (i != 1) { num *= i; i--; } return i; }

La progettazione di cui sopra non solo evita i limiti dello stack di chiamate, ma è anche molto più veloce perché non richiede nuovi contesti di funzione, il che significa che funzioni come quella di cui sopra verranno sottoposte a benchmark molto diversi su motori diversi.


In sostanza, queste differenze significano semplicemente che dovresti effettuare un benchmark su tutti i motori che ti aspetti eseguano il tuo codice per assicurarti che il codice che è veloce in uno non sia lento in un altro. Inoltre, se stai sviluppando una libreria che ti aspetti venga utilizzata su molte piattaforme, assicurati di includere motori più esoterici come Hermes ; hanno caratteristiche di prestazioni drasticamente diverse.

Menzioni d'onore

  • Il garbage collector e la sua tendenza a mettere tutto in pausa in modo casuale.


  • La capacità del compilatore JIT di eliminare tutto il codice perché "non è necessario".


  • Grafici a fiamma estremamente ampi nella maggior parte degli strumenti di sviluppo JavaScript.


  • Penso che tu abbia capito il punto.

Quindi… qual è la soluzione?

Vorrei poter indicare un pacchetto npm che risolva tutti questi problemi, ma in realtà non ne esiste uno.


Sul server, hai vita leggermente più facile. Puoi usare d8 per controllare manualmente i livelli di ottimizzazione, controllare il garbage collector e ottenere tempi precisi. Ovviamente, avrai bisogno di un po' di Bash-fu per impostare una pipeline di benchmark ben progettata per questo, poiché sfortunatamente, d8 non è ben integrato (o non è integrato affatto) con Node.js.


È anche possibile abilitare determinati flag in Node.js per ottenere risultati simili, ma si perderanno funzionalità come l'abilitazione di livelli di ottimizzazione specifici.

 v8 --sparkplug --always-sparkplug --no-opt [file]

Un esempio di D8 con un livello di compilazione specifico (sparkplug) abilitato. D8, di default, include un maggiore controllo di GC e più informazioni di debug in generale.


Puoi ottenere alcune funzionalità simili su JavaScriptCore??? Onestamente, non ho usato molto la CLI di JavaScriptCore, ed è fortemente poco documentata. Puoi abilitare livelli specifici usando i loro flag della riga di comando , ma non sono sicuro di quante informazioni di debug puoi recuperare. Bun include anche alcune utili utility di benchmarking , ma sono limitate in modo simile a Node.js.


Sfortunatamente, tutto questo richiede la versione base del motore/test del motore, che può essere piuttosto difficile da ottenere. Ho scoperto che il modo più semplice per gestire i motori è esvu abbinato a eshost-cli , poiché insieme semplificano notevolmente la gestione dei motori e l'esecuzione del codice su di essi. Naturalmente, è ancora richiesto molto lavoro manuale, poiché questi strumenti gestiscono solo l'esecuzione del codice su diversi motori: devi comunque scrivere tu stesso il codice di benchmarking.


Se stai solo cercando di fare il benchmark di un motore con opzioni predefinite il più accuratamente possibile sul server, ci sono strumenti Node.js standard come mitata che aiutano a migliorare la precisione del timing e gli errori correlati a GC. Molti di questi strumenti, come Mitata, possono essere utilizzati anche su molti motori; ovviamente, dovrai comunque impostare una pipeline come quella sopra.


Nel browser, tutto è molto più difficile. Non conosco soluzioni per tempi più precisi e il controllo del motore è molto più limitato. La maggior parte delle informazioni che puoi ottenere in relazione alle prestazioni di runtime di JavaScript nel browser saranno da Chrome devtools , che offre utilità di base per grafici di fiamma e simulazione di rallentamento della CPU.

Conclusione

Molte delle stesse decisioni di progettazione che hanno reso JavaScript (relativamente) performante e portabile rendono il benchmarking significativamente più difficile rispetto ad altri linguaggi. Ci sono molti più target da sottoporre a benchmarking e hai molto meno controllo su ogni target.


Speriamo che un giorno una soluzione semplifichi molti di questi problemi. Potrei anche creare uno strumento per semplificare il benchmarking cross-engine e a livelli di compilazione, ma per ora, creare una pipeline per risolvere tutti questi problemi richiede un bel po' di lavoro. Ovviamente, è importante ricordare che questi problemi non si applicano a tutti: se il tuo codice è in esecuzione solo in un ambiente, non perdere tempo a fare benchmarking in altri ambienti.


Indipendentemente da come scegli di fare il benchmark, spero che questo articolo ti abbia mostrato alcuni dei problemi presenti nel benchmarking JavaScript. Fammi sapere se un tutorial sull'implementazione di alcune delle cose che ho descritto sopra potrebbe esserti utile.