Perché il nuovo tipo di tupla in .Net 4.0 è un tipo di riferimento (classe) e non un tipo di valore (struttura)


89

Qualcuno conosce la risposta e / o ha un'opinione al riguardo?

Poiché le tuple normalmente non sarebbero molto grandi, presumo che avrebbe più senso usare gli struct rispetto alle classi per questi. Che ne dici?


1
Per chiunque inciampi qui dopo il 2016. In c # 7 e versioni successive, i letterali Tuple sono della famiglia di tipi ValueTuple<...>. Vedere il riferimento ai tipi di tupla C #
Tamir Daniely

Risposte:


94

Microsoft ha creato tutti i tipi di tupla tipi di riferimento per motivi di semplicità.

Personalmente penso che sia stato un errore. Le tuple con più di 4 campi sono molto insolite e dovrebbero essere sostituite comunque con un'alternativa più tipizzata (come un tipo di record in F #), quindi solo le piccole tuple sono di interesse pratico. I miei benchmark hanno mostrato che le tuple unboxed fino a 512 byte potrebbero essere ancora più veloci delle tuple in box.

Sebbene l'efficienza della memoria sia una delle preoccupazioni, credo che il problema dominante sia l'overhead del Garbage Collector .NET. L'allocazione e la raccolta sono molto costose su .NET perché il suo garbage collector non è stato ottimizzato molto (ad esempio rispetto alla JVM). Inoltre, il .NET GC (workstation) predefinito non è stato ancora parallelizzato. Di conseguenza, i programmi paralleli che utilizzano tuple si arrestano poiché tutti i core si contendono il garbage collector condiviso, distruggendo la scalabilità. Questa non è solo la preoccupazione dominante ma, per quanto ne so, è stata completamente trascurata da Microsoft quando ha esaminato questo problema.

Un'altra preoccupazione è l'invio virtuale. I tipi di riferimento supportano i sottotipi e, pertanto, i loro membri vengono generalmente richiamati tramite invio virtuale. Al contrario, i tipi di valore non possono supportare i sottotipi, quindi il richiamo dei membri è del tutto univoco e può sempre essere eseguito come chiamata di funzione diretta. L'invio virtuale è estremamente costoso sull'hardware moderno perché la CPU non può prevedere dove andrà a finire il contatore del programma. La JVM fa di tutto per ottimizzare l'invio virtuale, ma .NET non lo fa. Tuttavia, .NET fornisce una via di fuga dall'invio virtuale sotto forma di tipi di valore. Quindi rappresentare le tuple come tipi di valore potrebbe, ancora una volta, migliorare notevolmente le prestazioni qui. Ad esempio, chiamandoGetHashCode su una tupla 2 un milione di volte richiede 0.17s ma chiamarlo su una struttura equivalente richiede solo 0.008s, cioè il tipo di valore è 20 volte più veloce del tipo di riferimento.

Una situazione reale in cui questi problemi di prestazioni con le tuple si presentano comunemente è nell'uso delle tuple come chiavi nei dizionari. In realtà mi sono imbattuto in questo thread seguendo un collegamento dalla domanda Stack Overflow F # esegue il mio algoritmo più lentamente di Python! dove il programma F # dell'autore si è rivelato più lento del suo Python proprio perché utilizzava tuple in scatola. L'unboxing manuale utilizzando un structtipo scritto a mano rende il suo programma F # molte volte più veloce e più veloce di Python. Questi problemi non sarebbero mai sorti se le tuple fossero rappresentate da tipi di valore e non da tipi di riferimento per cominciare ...


2
@Bent: Sì, è esattamente quello che faccio quando mi imbatto in tuple su un percorso caldo in F #. Sarebbe bello se avessero fornito tuple sia in box che unboxed in .NET Framework però ...
JD

18
Per quanto riguarda l'invio virtuale, penso che la tua colpa sia fuori luogo: i Tuple<_,...,_>tipi avrebbero potuto essere sigillati, nel qual caso non sarebbe stato necessario alcun invio virtuale nonostante fossero tipi di riferimento. Sono più curioso del motivo per cui non sono sigillati rispetto al motivo per cui sono tipi di riferimento.
kvb

2
Dai miei test, per lo scenario in cui una tupla verrebbe generata in una funzione e restituita a un'altra funzione, e quindi non viene mai più utilizzata, le strutture a campo esposto sembrano offrire prestazioni superiori per elementi di dati di qualsiasi dimensione che non sono così enormi da saltare la pila. Le classi immutabili sono migliori solo se i riferimenti verranno passati abbastanza da giustificare il loro costo di costruzione (più grande è l'elemento di dati, meno devono essere passati perché il compromesso li favorisca). Poiché si suppone che una tupla rappresenti semplicemente un gruppo di variabili attaccate insieme, una struttura sembrerebbe ideale.
supercat

2
"le tuple unboxed fino a 512 byte potrebbero essere ancora più veloci di boxed" - quale scenario è? Si potrebbe essere in grado di assegnare una struct 512B più veloce di un'istanza della classe tenendo 512B dei dati, ma passando in giro sarebbe più di 100 volte più lento (presumendo x86). C'è qualcosa che sto trascurando?
Groo


45

Il motivo è molto probabile perché solo le tuple più piccole avrebbero senso come tipi di valore poiché avrebbero un footprint di memoria ridotto. Le tuple più grandi (cioè quelle con più proprietà) avrebbero effettivamente sofferto in termini di prestazioni poiché sarebbero più grandi di 16 byte.

Piuttosto che avere alcune tuple come tipi di valore e altre essere tipi di riferimento e costringere gli sviluppatori a sapere quali sono quali immagino che la gente di Microsoft pensasse che renderli tutti i tipi di riferimento fosse più semplice.

Ah, sospetti confermati! Vedere Creazione di tupla :

La prima decisione importante era se trattare le tuple come tipo di riferimento o valore. Poiché sono immutabili ogni volta che si desidera modificare i valori di una tupla, è necessario crearne una nuova. Se sono tipi di riferimento, significa che possono essere generati molti rifiuti se si modificano gli elementi in una tupla in un ciclo stretto. Le tuple F # erano tipi di riferimento, ma il team aveva la sensazione che avrebbero potuto realizzare un miglioramento delle prestazioni se due, e forse tre, tuple di elemento fossero invece tipi di valore. Alcuni team che avevano creato tuple interne avevano usato il valore invece dei tipi di riferimento, perché i loro scenari erano molto sensibili alla creazione di molti oggetti gestiti. Hanno scoperto che l'utilizzo di un tipo di valore dava loro prestazioni migliori. Nella nostra prima bozza della specifica della tupla, abbiamo mantenuto le tuple a due, tre e quattro elementi come tipi di valore, con il resto come tipi di riferimento. Tuttavia, durante una riunione di progettazione che includeva rappresentanti di altre lingue, è stato deciso che questo progetto "diviso" sarebbe stato fonte di confusione, a causa della semantica leggermente diversa tra i due tipi. È stato stabilito che la coerenza nel comportamento e nel design ha una priorità maggiore rispetto ai potenziali aumenti delle prestazioni. In base a questo input, abbiamo modificato il design in modo che tutte le tuple siano tipi di riferimento, sebbene abbiamo chiesto al team di F # di eseguire alcune indagini sulle prestazioni per vedere se si è verificato un aumento della velocità quando si utilizza un tipo di valore per alcune dimensioni di tuple. Aveva un buon modo per testarlo, dal momento che il suo compilatore, scritto in F #, era un buon esempio di un programma di grandi dimensioni che utilizzava tuple in una varietà di scenari. Alla fine, il team di F # ha scoperto che non ha ottenuto un miglioramento delle prestazioni quando alcune tuple erano tipi di valore anziché tipi di riferimento. Questo ci ha fatto sentire meglio riguardo alla nostra decisione di utilizzare i tipi di riferimento per la tupla.



Ah! Capisco. Sono ancora un po 'confuso sul fatto che i tipi di valore non significano nulla in pratica qui: P
Bent Rasmussen

Ho appena letto il commento sull'assenza di interfacce generiche e quando ho guardato il codice in precedenza è stata esattamente un'altra cosa che mi ha colpito. Non è davvero interessante quanto siano generici i tipi di Tuple. Ma immagino che tu possa sempre crearne uno tuo ... Non c'è comunque supporto sintattico in C #. Eppure almeno ... Tuttavia, l'uso di generici e i vincoli che ha sembra ancora limitato limitato in .Net. C'è un potenziale sostanziale per librerie molto generiche molto astratte, ma i generici probabilmente hanno bisogno di cose extra come i tipi di ritorno covarianti.
Bent Rasmussen

7
Il tuo limite di "16 byte" è falso. Quando l'ho testato su .NET 4, ho scoperto che il GC è così lento che le tuple unboxed fino a 512 byte possono essere ancora più veloci. Metterei anche in dubbio i risultati dei benchmark di Microsoft. Scommetto che hanno ignorato il parallelismo (il compilatore F # non è parallelo) ed è qui che evitare GC paga davvero perché anche il GC della workstation .NET non è parallelo.
JD

Per curiosità, mi chiedo se il team del compilatore abbia testato l'idea di rendere le tuple strutture EXPOSED-FIELD ? Se si ha un'istanza di un tipo con vari tratti e ha bisogno di un'istanza che è identica tranne che per un tratto che è diverso, una struttura a campo esposto può farlo molto più velocemente di qualsiasi altro tipo, e il vantaggio cresce solo quando le strutture si ottengono più grande.
supercat

7

Se i tipi .NET System.Tuple <...> fossero definiti come strutture, non sarebbero scalabili. Ad esempio, una tupla ternaria di interi lunghi scala attualmente come segue:

type Tuple3 = System.Tuple<int64, int64, int64>
type Tuple33 = System.Tuple<Tuple3, Tuple3, Tuple3>
sizeof<Tuple3> // Gets 4
sizeof<Tuple33> // Gets 4

Se la tupla ternaria fosse definita come una struttura, il risultato sarebbe il seguente (basato su un esempio di test che ho implementato):

sizeof<Tuple3> // Would get 32
sizeof<Tuple33> // Would get 104

Poiché le tuple hanno il supporto della sintassi incorporato in F # e sono usate molto spesso in questo linguaggio, le tuple "struct" metterebbero i programmatori di F # a rischio di scrivere programmi inefficienti senza nemmeno esserne consapevoli. Sarebbe così facile:

let t3 = 1L, 2L, 3L
let t33 = t3, t3, t3

A mio parere, le tuple "struct" causerebbero un'alta probabilità di creare significative inefficienze nella programmazione quotidiana. D'altra parte, le tuple di "classe" attualmente esistenti causano anche alcune inefficienze, come menzionato da @Jon. Tuttavia, penso che il prodotto della "probabilità di occorrenza" per il "danno potenziale" sarebbe molto più alto con le strutture di quanto non lo sia attualmente con le classi. Pertanto, l'attuale implementazione è il male minore.

Idealmente, ci sarebbero sia tuple "class" che tuple "struct", entrambe con supporto sintattico in F #!

Modifica (07/10/2017)

Le tuple Struct sono ora completamente supportate come segue:


2
Se si evita la copia non necessaria, una struttura di campo esposto di qualsiasi dimensione sarà più efficiente di una classe immutabile della stessa dimensione, a meno che ogni istanza non venga copiata abbastanza volte che il costo di tale copia superi il costo della creazione di un oggetto heap (il il numero di copie in pareggio varia in base alle dimensioni dell'oggetto). Tale copia può essere inevitabile se si desidera una struttura che finge di essere immutabile, ma le strutture progettate per apparire come raccolte di variabili (che è ciò che sono ) possono essere utilizzate in modo efficiente anche quando sono enormi.
supercat

2
Può essere che F # non giochi bene con l'idea di passare le strutture ref, o potrebbe non piacere il fatto che le cosiddette "strutture immutabili" non lo siano, specialmente quando sono inscatolate. Peccato che .net non abbia mai implementato il concetto di passaggio di parametri da parte di un enforceable const ref, poiché in molti casi tale semantica è ciò che è veramente richiesto.
supercat

1
Per inciso, considero il costo ammortizzato di GC come parte del costo di allocazione degli oggetti; se fosse necessario un GC L0 dopo ogni megabyte di allocazioni, allora il costo di allocare 64 byte è circa 1 / 16.000 del costo di un GC L0, più una frazione del costo di qualsiasi GC L1 o L2 che si rende necessario come conseguenza di esso.
supercat

4
"Penso che il prodotto della probabilità di accadimento per il potenziale danno sarebbe molto più alto con le strutture di quanto non lo sia attualmente con le classi". FWIW, ho visto molto raramente tuple di tuple in natura e le considero un difetto di progettazione, ma molto spesso vedo persone che lottano con prestazioni terribili quando usano le tuple (ref) come chiavi in ​​a Dictionary, ad esempio qui: stackoverflow.com/questions/5850243 /…
JD

3
@ Jon Sono passati due anni da quando ho scritto questa risposta, e ora sono d'accordo con te sul fatto che sarebbe preferibile se almeno 2- e 3-tuple fossero strutture. A questo proposito è stato suggerito un suggerimento vocale dell'utente in lingua F # . La questione ha una certa urgenza, poiché negli ultimi anni c'è stata una massiccia crescita di applicazioni in big data, finanza quantitativa e giochi.
Marc Sigrist

4

Per 2-tuple, puoi sempre usare KeyValuePair <TKey, TValue> dalle versioni precedenti del Common Type System. È un tipo di valore.

Un piccolo chiarimento all'articolo di Matt Ellis sarebbe che la differenza nella semantica d'uso tra i tipi di riferimento e valore è "lieve" solo quando l'immutabilità è attiva (cosa che, ovviamente, sarebbe il caso qui). Tuttavia, penso che sarebbe stato meglio nel progetto BCL non introdurre la confusione di avere Tuple incrociato con un tipo di riferimento a una certa soglia.


Se un valore verrà utilizzato una volta dopo essere stato restituito, una struttura di campo esposto di qualsiasi dimensione supererà qualsiasi altro tipo, a condizione che non sia così mostruosamente enorme da far saltare lo stack. Il costo della creazione di un oggetto classe verrà recuperato solo se il riferimento finisce per essere condiviso più volte. Ci sono volte in cui è utile che un tipo eterogeneo a dimensione fissa per scopi generici sia una classe, ma ci sono altre volte in cui una struttura sarebbe migliore, anche per cose "grandi".
supercat

Grazie per aver aggiunto questa utile regola pratica. Spero comunque che tu non abbia frainteso la mia posizione: sono un drogato di valore. ( stackoverflow.com/a/14277068 non dovrebbe lasciare dubbi).
Glenn Slayden

I tipi di valore sono una delle grandi caratteristiche di .net, ma sfortunatamente la persona che ha scritto il msdn dox non è riuscita a riconoscere che ci sono più casi di utilizzo disgiunti per loro e che diversi casi di utilizzo dovrebbero avere linee guida diverse. Lo stile di struct consigliato da msdn dovrebbe essere usato solo con gli struct che rappresentano un valore omogeneo, ma se è necessario rappresentare alcuni valori indipendenti fissati insieme con del nastro adesivo, non si dovrebbe usare quello stile di struct - si dovrebbe usare uno struct con campi pubblici esposti.
supercat

0

Non lo so, ma se hai mai usato F # Le tuple fanno parte del linguaggio. Se ho creato un .dll e restituito un tipo di tuple, sarebbe bello avere un tipo in cui inserirlo. Sospetto ora che F # fa parte del linguaggio (.Net 4) alcune modifiche a CLR sono state apportate per ospitare alcune strutture comuni in fa #

Da http://en.wikibooks.org/wiki/F_Sharp_Programming/Tuples_and_Records

let scalarMultiply (s : float) (a, b, c) = (a * s, b * s, c * s);;

val scalarMultiply : float -> float * float * float -> float * float * float

scalarMultiply 5.0 (6.0, 10.0, 20.0);;
val it : float * float * float = (30.0, 50.0, 100.0)
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.