L'immutabilità danneggia le prestazioni in JavaScript?


88

Sembra che ci sia una tendenza recente in JavaScript verso il trattamento delle strutture di dati come immutabili. Ad esempio, se è necessario modificare una singola proprietà di un oggetto, è preferibile creare un oggetto completamente nuovo con la nuova proprietà e copiare semplicemente tutte le altre proprietà dal vecchio oggetto e lasciare che il vecchio oggetto venga raccolto. (Questa è la mia comprensione comunque.)

La mia reazione iniziale è che suona male per le prestazioni.

Ma poi librerie come Immutable.js e Redux.js sono scritti da persone più intelligenti di me, e sembrano avere una forte preoccupazione per le prestazioni, per cui mi domando se la mia comprensione della spazzatura (e il suo impatto sulle prestazioni) è sbagliato.

Ci sono benefici prestazionali per l'immutabilità che mi mancano e superano gli svantaggi della creazione di così tanta spazzatura?


8
Hanno una forte preoccupazione per le prestazioni in parte perché l'immutabilità (a volte) ha un costo per le prestazioni e vogliono minimizzare il costo delle prestazioni il più possibile. L'immutabilità, di per sé, ha solo vantaggi in termini di prestazioni, nel senso che semplifica la scrittura di codice multi-thread.
Robert Harvey,

8
Nella mia esperienza, le prestazioni sono solo una preoccupazione valida per due scenari - uno, quando un'azione viene eseguita più di 30 volte in un secondo, e due - quando i suoi effetti aumentano con ogni esecuzione (Windows XP una volta trovò un bug che impiegava il tempo di Windows Update O(pow(n, 2))per ogni aggiornamento nella sua storia .) La maggior parte degli altri codici è una risposta immediata a un evento; un clic, una richiesta API o simili, e fintanto che il tempo di esecuzione è costante, la pulizia di qualsiasi numero di oggetti non ha importanza.
Katana314,

4
Inoltre, considera che esistono implementazioni efficienti di strutture di dati immutabili. Forse questi non sono così efficienti come quelli mutabili, ma probabilmente ancora più efficienti di un'implementazione ingenua. Vedi ad esempio Strutture di dati puramente funzionali di Chris Okasaki
Giorgio,

1
@ Katana314: 30+ volte per me non sarebbero ancora sufficienti per giustificare la preoccupazione per le prestazioni. Ho portato un piccolo emulatore di CPU che ho scritto su node.js e il nodo ha eseguito la CPU virtuale a circa 20 MHz (20 milioni di volte al secondo). Quindi mi preoccuperei delle prestazioni solo se stavo facendo qualcosa più di 1000 volte al secondo (anche allora, non mi preoccuperei davvero fino a quando non eseguirò 1000000 operazioni al secondo perché so di poterne fare comodamente più di 10 contemporaneamente) .
Slebetman,

2
@RobertHarvey "L'immutabilità, di per sé, ha solo vantaggi in termini di prestazioni, nel senso che semplifica la scrittura di codice multi-thread." Ciò non è del tutto vero, l'immutabilità consente una condivisione molto pervasiva senza conseguenze reali. Che è molto pericoloso in un ambiente mutevole. Questo ti dà l'idea di una O(1)suddivisione in array e di O(log n)inserirli in un albero binario pur essendo in grado di usare liberamente quello vecchio, e un altro esempio è tailsche prendere tutte le code di un elenco tails [1, 2] = [[1, 2], [2], []]richiede solo O(n)tempo e spazio, ma è O(n^2)nel conteggio degli elementi
punto

Risposte:


59

Ad esempio, se è necessario modificare una singola proprietà di un oggetto, è preferibile creare un oggetto completamente nuovo con la nuova proprietà e copiare semplicemente tutte le altre proprietà dal vecchio oggetto e lasciare che il vecchio oggetto venga raccolto.

Senza immutabilità, potresti dover passare un oggetto tra diversi ambiti e non sai in anticipo se e quando l'oggetto verrà modificato. Quindi, per evitare effetti collaterali indesiderati, si inizia a creare una copia completa dell'oggetto "per ogni evenienza" e si passa quella copia, anche se risulta che nessuna proprietà deve essere cambiata affatto. Ciò lascerà molta più spazzatura che nel tuo caso.

Ciò dimostra ciò: se crei il giusto scenario ipotetico, puoi provare qualsiasi cosa, specialmente quando si tratta di prestazioni. Il mio esempio, tuttavia, non è così ipotetico come potrebbe sembrare. Ho lavorato il mese scorso su un programma in cui ci siamo imbattuti esattamente in quel problema perché inizialmente avevamo deciso di non utilizzare una struttura di dati immutabile, e ho esitato a riformattarlo in seguito perché non sembrava valesse la pena.

Quindi, quando guardi casi come questo da un post SO più vecchio , la risposta alle tue domande diventa probabilmente chiara - dipende . In alcuni casi l'immutabilità danneggerà le prestazioni, per alcuni potrebbe essere vero il contrario, per molti casi dipenderà da quanto sia intelligente la tua implementazione e per ancora più la differenza sarà trascurabile.

Un'ultima nota: un problema del mondo reale che potresti incontrare è che devi decidere in anticipo a favore o contro l'immutabilità per alcune strutture di dati di base. Quindi costruisci molto codice su questo e diverse settimane o mesi dopo vedrai se la decisione è stata buona o cattiva.

La mia regola personale per questa situazione è:

  • Se si progetta una struttura di dati con solo pochi attributi basati su tipi primitivi o di altri tipi immutabili, provare prima l'immutabilità.
  • Se si desidera progettare un tipo di dati in cui sono coinvolti array con dimensioni grandi (o non definite), l'accesso casuale e la modifica dei contenuti, utilizzare la mutabilità.

Per situazioni tra questi due estremi, usa il tuo giudizio. Ma YMMV.


8
That will leave a lot more garbage than in your case.e a peggiorare le cose, il tuo runtime probabilmente non sarà in grado di rilevare la duplicazione inutile, e quindi (a differenza di un oggetto immutabile scaduto che nessuno sta usando) non sarà nemmeno idoneo per la raccolta.
Jacob Raihle l'

37

Prima di tutto, la tua caratterizzazione di strutture di dati immutabili è imprecisa. In generale, la maggior parte di una struttura di dati non viene copiata, ma condivisa e vengono copiate solo le parti modificate. Si chiama struttura di dati persistente . La maggior parte delle implementazioni è in grado di sfruttare la maggior parte delle strutture di dati persistenti. Le prestazioni sono abbastanza vicine alle strutture di dati mutabili che i programmatori funzionali generalmente le considerano trascurabili.

In secondo luogo, trovo che molte persone hanno un'idea abbastanza imprecisa della vita tipica degli oggetti nei tipici programmi imperativi. Forse questo è dovuto alla popolarità dei linguaggi gestiti dalla memoria. Siediti qualche volta e osserva davvero quanti oggetti temporanei e copie difensive crei rispetto alle strutture dati di lunga durata. Penso che rimarrai sorpreso dal rapporto.

Ho avuto osservazioni da parte della gente in classi di programmazione funzionale a cui insegno quanta spazzatura crea un algoritmo, quindi mostro la tipica versione imperativa dello stesso algoritmo che crea altrettanto. Solo per qualche motivo la gente non se ne accorge più.

Incoraggiando la condivisione e scoraggiando la creazione di variabili fino a quando non si ha un valore valido da inserire, l'immutabilità tende a incoraggiare pratiche di codifica più pulite e strutture di dati più durature. Ciò porta spesso a livelli comparabili se non inferiori di immondizia, a seconda dell'algoritmo.


8
"... poi mostro la tipica versione imperativa dello stesso algoritmo che crea altrettanto". Questo. Inoltre, le persone che sono nuove a questo stile, e soprattutto se sono nuove allo stile funzionale in generale, possono inizialmente produrre implementazioni funzionali non ottimali.
wberry,

1
"scoraggiare la creazione di variabili" Non è valido solo per le lingue in cui il comportamento predefinito è la copia su assegnazione / costruzione implicita? In JavaScript, una variabile è solo un identificatore; non è un oggetto a sé stante. Occupa ancora spazio da qualche parte, ma è trascurabile (soprattutto perché la maggior parte delle implementazioni di JavaScript, per quanto ne sappia, usa ancora uno stack per le chiamate di funzione, il che significa che a meno che tu non abbia molta ricorsione finirai per riutilizzare lo stesso spazio dello stack per la maggior parte variabili temporanee). L'immutabilità non ha alcuna relazione con quell'aspetto.
JAB

33

Sono arrivato in ritardo a queste domande e risposte con già ottime risposte, ma volevo intromettermi come uno straniero abituato a guardare le cose dal punto di vista di livello inferiore di bit e byte in memoria.

Sono molto entusiasta di progetti immutabili, anche provenienti da una prospettiva C e dalla prospettiva di trovare nuovi modi per programmare efficacemente questo hardware bestiale che abbiamo in questi giorni.

Più lento / veloce

Quanto alla domanda se rende le cose più lente, sarebbe una risposta robotica yes. A questo tipo di livello concettuale molto tecnico, l'immutabilità non può che rallentare le cose. L'hardware fa meglio quando non sta allocando sporadicamente la memoria e può semplicemente modificare la memoria esistente (perché abbiamo concetti come la località temporale).

Eppure una risposta pratica è maybe. Le prestazioni sono ancora in gran parte una metrica di produttività in qualsiasi base di codice non banale. In genere non riteniamo che le basi di codice orribili da mantenere che inciampano nelle condizioni di gara siano le più efficienti, anche se ignoriamo i bug. L'efficienza è spesso una funzione di eleganza e semplicità. Il picco delle micro-ottimizzazioni può in qualche modo essere in conflitto, ma quelle sono generalmente riservate alle sezioni di codice più piccole e più critiche.

Trasformazione di bit e byte immutabili

Venendo dal punto di vista di basso livello, se riusciamo a fare radiografie su concetti come objectse stringscosì via, al centro di esso ci sono solo bit e byte in varie forme di memoria con caratteristiche di velocità / dimensione diverse (la velocità e le dimensioni dell'hardware di memoria sono in genere si escludono a vicenda).

inserisci qui la descrizione dell'immagine

Alla gerarchia di memoria del computer piace quando accediamo ripetutamente allo stesso blocco di memoria, come nel diagramma sopra, poiché manterrà quel blocco di memoria a cui si accede frequentemente nella forma di memoria più veloce (cache L1, ad esempio, che è quasi veloce come un registro). Potremmo accedere ripetutamente alla stessa identica memoria (riutilizzandola più volte) o accedere ripetutamente a diverse sezioni del blocco (es: scorrere gli elementi in un blocco contiguo che accede ripetutamente a varie sezioni di quel blocco di memoria).

Finiamo per lanciare una chiave in quel processo se la modifica di questa memoria finisce per voler creare un blocco di memoria completamente nuovo sul lato, in questo modo:

inserisci qui la descrizione dell'immagine

... in questo caso, l'accesso al nuovo blocco di memoria potrebbe richiedere errori di pagina obbligatori e errori di cache per spostarlo indietro nelle forme di memoria più veloci (fino in fondo in un registro). Questo può essere un vero killer delle prestazioni.

Esistono modi per mitigarlo, tuttavia, utilizzando un pool di riserva di memoria preallocata, già toccato.

Grandi aggregati

Un altro problema concettuale che emerge da una visione di livello leggermente superiore è semplicemente fare copie non necessarie di aggregati davvero grandi alla rinfusa.

Per evitare un diagramma troppo complesso, immaginiamo che questo semplice blocco di memoria fosse in qualche modo costoso (forse caratteri UTF-32 su un hardware incredibilmente limitato).

inserisci qui la descrizione dell'immagine

In questo caso, se volessimo sostituire "HELP" con "KILL" e questo blocco di memoria fosse immutabile, dovremmo creare un blocco completamente nuovo nella sua interezza per creare un nuovo oggetto unico, anche se solo alcune parti sono cambiate :

inserisci qui la descrizione dell'immagine

Allungando un po 'la nostra immaginazione, questo tipo di copia profonda di tutto il resto solo per rendere unica una piccola parte potrebbe essere piuttosto costoso (nei casi del mondo reale, questo blocco di memoria sarebbe molto, molto più grande per rappresentare un problema).

Tuttavia, nonostante tale spesa, questo tipo di design tenderà ad essere molto meno soggetto all'errore umano. Chiunque abbia lavorato in un linguaggio funzionale con funzioni pure può probabilmente apprezzarlo, specialmente in casi multithread in cui è possibile multithreading di tale codice senza preoccuparsi del mondo. In generale, i programmatori umani tendono a inciampare sui cambiamenti di stato, in particolare quelli che causano effetti collaterali esterni a stati al di fuori dell'ambito di una funzione corrente. Anche il recupero da un errore esterno (eccezione) in tal caso può essere incredibilmente difficile con cambiamenti di stato esterni mutabili nel mix.

Un modo per mitigare questo lavoro di copia ridondante è trasformare questi blocchi di memoria in una raccolta di puntatori (o riferimenti) a caratteri, in questo modo:

Mi scuso, non sono riuscito a rendermi conto che non abbiamo bisogno di rendere Lunico durante la realizzazione del diagramma.

Il blu indica i dati copiati poco profondi.

inserisci qui la descrizione dell'immagine

... sfortunatamente, sarebbe incredibilmente costoso pagare un puntatore / costo di riferimento per personaggio. Inoltre, potremmo spargere il contenuto dei personaggi in tutto lo spazio degli indirizzi e finire per pagarlo sotto forma di un carico di errori di pagina e mancati riscontri nella cache, rendendo questa soluzione ancora peggiore della copia dell'intera cosa nella sua interezza.

Anche se siamo stati attenti a allocare questi caratteri in modo contiguo, supponiamo che la macchina possa caricare 8 caratteri e 8 puntatori su un carattere in una riga della cache. Finiamo per caricare la memoria in questo modo per attraversare la nuova stringa:

inserisci qui la descrizione dell'immagine

In questo caso, finiamo per caricare 7 diverse righe di cache di memoria contigua da caricare per attraversare questa stringa, quando idealmente ne abbiamo bisogno solo 3.

Raccogliere i dati

Per mitigare il problema sopra riportato, possiamo applicare la stessa strategia di base ma a un livello più grossolano di 8 caratteri, ad es

inserisci qui la descrizione dell'immagine

Il risultato richiede il caricamento di 4 righe di cache di dati (1 per i 3 puntatori e 3 per i caratteri) per attraversare questa stringa che è solo 1 in meno dell'ottimale teorico.

Quindi non è affatto male. C'è un po 'di spreco di memoria, ma la memoria è abbondante e l'utilizzo di più non rallenta le cose se la memoria aggiuntiva sarà solo dati freddi a cui non si accede frequentemente. È solo per i dati caldi e contigui in cui l'uso ridotto di memoria e la velocità spesso vanno di pari passo dove vogliamo adattare una quantità maggiore di memoria in una singola pagina o linea di cache e accedervi tutte prima dello sfratto. Questa rappresentazione è piuttosto adatta alla cache.

Velocità

Quindi l'utilizzo di una rappresentazione come sopra può dare un discreto equilibrio di prestazioni. Probabilmente gli usi più critici in termini di prestazioni di strutture di dati immutabili assumeranno questa natura di modifica di grossi pezzi di dati e di renderli unici nel processo, mentre copiano superficialmente pezzi non modificati. Implica anche un certo sovraccarico di operazioni atomiche per fare riferimento in sicurezza ai pezzi copiati poco profondi in un contesto multithread (probabilmente con un conteggio dei riferimenti atomici in corso).

Tuttavia, fintanto che questi grossi pezzi di dati sono rappresentati a un livello abbastanza approssimativo, molte di queste spese generali diminuiscono e possono anche essere banalizzate, pur continuando a darci la sicurezza e la facilità di codifica e multithreading di più funzioni in una forma pura senza lato esterno effetti.

Conservazione di dati nuovi e vecchi

Quando vedo l'immutabilità come potenzialmente più utile dal punto di vista delle prestazioni (in senso pratico) è quando possiamo essere tentati di fare copie complete di dati di grandi dimensioni per renderlo unico in un contesto mutevole in cui l'obiettivo è produrre qualcosa di nuovo da qualcosa che esiste già in un modo in cui vogliamo mantenere sia vecchi che nuovi, quando potremmo semplicemente renderne piccoli pezzi unici con un design accurato e immutabile.

Esempio: Annulla sistema

Un esempio di ciò è un sistema di annullamento. Potremmo cambiare una piccola parte di una struttura di dati e desiderare mantenere sia il modulo originale che possiamo annullare, sia il nuovo modulo. Con questo tipo di design immutabile che rende uniche solo piccole sezioni modificate della struttura dei dati, possiamo semplicemente archiviare una copia dei vecchi dati in una voce di annullamento, pagando solo il costo della memoria dei dati delle porzioni uniche aggiunte. Ciò fornisce un equilibrio molto efficace di produttività (rendendo l'implementazione di un sistema di annullamento un gioco da ragazzi) e prestazioni.

Interfacce di alto livello

Eppure qualcosa di imbarazzante sorge con il caso precedente. In un tipo locale di contesto di funzioni, i dati mutabili sono spesso i più semplici e semplici da modificare. Dopotutto, il modo più semplice per modificare un array è spesso semplicemente eseguirne il ciclo e modificare un elemento alla volta. Possiamo finire per aumentare il sovraccarico intellettuale se avessimo un gran numero di algoritmi di alto livello tra cui scegliere per trasformare un array e dovessimo scegliere quello appropriato per garantire che tutte queste copie poco profonde vengano fatte mentre le parti che vengono modificate sono reso unico.

Probabilmente il modo più semplice in quei casi è usare localmente buffer mutabili all'interno del contesto di una funzione (dove di solito non ci fanno inciampare) che commettono modifiche atomiche alla struttura dei dati per ottenere una nuova copia immutabile (credo che alcune lingue chiamino questi "transitori") ...

... o potremmo semplicemente modellare funzioni di trasformazione di livello superiore e superiore sui dati in modo da poter nascondere il processo di modifica di un buffer mutabile e di impegnarlo nella struttura senza la logica mutabile coinvolta. In ogni caso, questo non è ancora un territorio ampiamente esplorato e abbiamo il nostro lavoro tagliato se abbracciamo progetti immutabili più per trovare interfacce significative per come trasformare queste strutture di dati.

Strutture dati

Un'altra cosa che sorge qui è che l'immutabilità usata in un contesto critico per le prestazioni probabilmente vorrà che le strutture di dati si rompano in dati pesanti in cui i blocchi non sono di dimensioni troppo ridotte ma anche non troppo grandi.

Gli elenchi collegati potrebbero voler cambiare un po 'per adattarli e trasformarsi in elenchi non srotolati. Le matrici grandi e contigue potrebbero trasformarsi in una matrice di puntatori in blocchi contigui con l'indicizzazione del modulo per l'accesso casuale.

Cambia potenzialmente il modo in cui guardiamo le strutture di dati in un modo interessante, spingendo al contempo le funzioni di modifica di queste strutture di dati per assomigliare ad una natura più voluminosa per nascondere la complessità aggiuntiva nel copiare superficialmente alcuni bit qui e renderne altri unici lì.

Prestazione

Comunque, questa è la mia piccola visione di livello inferiore sull'argomento. Teoricamente, l'immutabilità può avere un costo che va da molto grande a più piccolo. Ma un approccio molto teorico non sempre fa accelerare le applicazioni. Potrebbe renderli scalabili, ma la velocità del mondo reale spesso richiede di abbracciare la mentalità più pratica.

Da un punto di vista pratico, qualità come prestazioni, manutenibilità e sicurezza tendono a trasformarsi in una grande sfocatura, soprattutto per una base di codice molto ampia. Mentre le prestazioni in un certo senso sono degradate dall'immutabilità, è difficile sostenere i vantaggi che ha sulla produttività e sulla sicurezza (compresa la sicurezza dei thread). Con un aumento a questi può spesso derivare un aumento delle prestazioni pratiche, anche solo perché gli sviluppatori hanno più tempo per ottimizzare e ottimizzare il loro codice senza essere sciamati da bug.

Quindi, dal punto di vista pratico, penso che strutture di dati immutabili potrebbero effettivamente aiutare le prestazioni in molti casi, per quanto strano possa sembrare. Un mondo ideale potrebbe cercare una combinazione di questi due: strutture di dati immutabili e mutabili, con quelle mutabili che in genere sono molto sicure da usare in un ambito molto locale (es: locale a una funzione), mentre quelle immutabili possono evitare il lato esterno effetti e trasformare tutte le modifiche in una struttura di dati in un'operazione atomica producendo una nuova versione senza rischio di condizioni di gara.


11

ImmutableJS è in realtà abbastanza efficiente. Se prendiamo un esempio:

var x = {
    Foo: 1,
    Bar: { Baz: 2 }
    Qux: { AnotherVal: 3 }
}

Se l'oggetto sopra è reso immutabile, allora modifichi il valore della proprietà 'Baz' quello che otterrai:

var y = x.setIn('/Bar/Baz', 3);
y !== x; // Different object instance
y.Bar !== x.Bar // As the Baz property was changed, the Bar object is a diff instance
y.Qux === y.Qux // Qux is the same object instance

Ciò crea alcuni miglioramenti delle prestazioni davvero interessanti per i modelli di oggetti profondi, in cui è necessario solo copiare i tipi di valore sugli oggetti nel percorso della radice. Maggiore è il modello a oggetti e minori saranno le modifiche apportate, migliori saranno le prestazioni di memoria e CPU della struttura dei dati immutabili man mano che finiscono per condividere molti oggetti.

Come hanno detto le altre risposte, se si contrappone questo tentativo di fornire le stesse garanzie copiando in modo difensivo xprima di passare a una funzione che potrebbe manipolarlo, le prestazioni sono significativamente migliori.


4

In linea retta, il codice immutabile ha il sovraccarico della creazione di oggetti, che è più lento. Tuttavia, ci sono molte situazioni in cui il codice mutabile diventa molto difficile da gestire in modo efficiente (con conseguente copiatura difensiva, che è troppo costosa), e ci sono molte strategie intelligenti per mitigare il costo della "copia" di un oggetto , come menzionato da altri.

Se hai un oggetto come un contatore e viene incrementato molte volte al secondo, avere quel contatore immutabile potrebbe non valere la pena di prestazione. Se hai un oggetto che viene letto da molte parti diverse della tua applicazione, e ognuno di loro vuole avere il proprio clone leggermente diverso dell'oggetto, avrai un tempo molto più facile orchestrarlo in modo performante usando un buon implementazione di oggetti immutabili.


4

Per aggiungere a questa domanda (già eccellente risposta):

La risposta breve è ; danneggerà le prestazioni perché stai creando sempre e solo oggetti invece di mutare quelli esistenti, con conseguente sovraccarico di creazione di oggetti.


Tuttavia, la risposta lunga non è davvero .

Dal vero punto di vista del runtime, in JavaScript crei già molti oggetti runtime: funzioni e valori letterali degli oggetti sono ovunque in JavaScript e nessuno sembra pensarci due volte sull'uso di questi. Direi che la creazione di oggetti è in realtà abbastanza economica, anche se non ho citazioni per questo, quindi non la userei come argomento autonomo.

Per me, il più grande aumento delle "prestazioni" non è nelle prestazioni di runtime ma nelle prestazioni degli sviluppatori . Una delle prime cose che ho imparato lavorando su applicazioni del mondo reale (TM) è che la mutabilità è davvero pericolosa e confusa. Ho perso molte ore inseguendo un thread (non il tipo di concorrenza) di esecuzione cercando di capire cosa sta causando un bug oscuro quando si scopre essere una mutazione dall'altra parte della dannata applicazione!

L'uso dell'immutabilità rende le cose molto più facili da ragionare. Sei in grado di sapere immediatamente che l'oggetto X non cambierà durante la sua vita, e l'unico modo per farlo è clonarlo. Lo apprezzo molto di più (specialmente negli ambienti di squadra) di qualsiasi microottimizzazione che la mutabilità potrebbe portare.

Vi sono eccezioni, in particolare le strutture di dati come indicato sopra. Raramente mi sono imbattuto in uno scenario in cui ho voluto modificare una mappa dopo la creazione (anche se è vero che sto parlando di mappe pseudo-oggetto-letterali piuttosto che di mappe ES6), lo stesso per le matrici. Quando hai a che fare con strutture dati più grandi, la mutabilità potrebbe ripagare. Ricorda che ogni oggetto in JavaScript viene passato come riferimento anziché come valore.


Detto questo, un punto sollevato sopra era il GC e la sua incapacità di rilevare duplicati. Questa è una preoccupazione legittima, ma secondo me è solo una preoccupazione quando la memoria è una preoccupazione, e ci sono modi molto più facili per codificarti in un angolo - per esempio, riferimenti circolari nelle chiusure.


In definitiva, io preferirei avere una base di codice immutabile, con molto pochi (se presente) sezioni mutevoli e di essere un po 'meno performante che avere mutevolezza ovunque. Puoi sempre ottimizzare in seguito se l'immutabilità, per qualche motivo, diventa un problema per le prestazioni.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.