Strutture contro classi


93

Sto per creare 100.000 oggetti nel codice. Sono piccoli, solo con 2 o 3 proprietà. Li inserirò in un elenco generico e quando lo saranno, li collegherò in loop e controllerò il valore ae forse aggiornerò il valore b.

È più veloce / migliore creare questi oggetti come classe o come struttura?

MODIFICARE

un. Le proprietà sono tipi di valore (eccetto la stringa che penso?)

b. Potrebbero (non siamo ancora sicuri) avere un metodo di convalida

MODIFICA 2

Mi chiedevo: gli oggetti nell'heap e nello stack vengono elaborati allo stesso modo dal Garbage Collector o funziona in modo diverso?


2
Avranno solo campi pubblici o avranno anche metodi? I tipi sono tipi primitivi, come gli interi? Saranno contenuti in un array o in qualcosa di simile a List <T>?
JeffFerguson

14
Un elenco di strutture mutabili? Attento al velociraptor.
Anthony Pegram

1
@ Anthony: temo che mi manchi la battuta del velociraptor: -s
Michel

5
Lo scherzo del velociraptor è di XKCD. Ma quando rilasci i dettagli errati / dettagli sull'implementazione 'i tipi di valore sono allocati nello stack' (eliminare se applicabile), allora è Eric Lippert a cui devi fare attenzione ...
Greg Beech

Risposte:


137

È più veloce creare questi oggetti come classe o come struttura?

Sei l'unica persona che può determinare la risposta a questa domanda. Provalo in entrambi i modi, misura una metrica delle prestazioni significativa, incentrata sull'utente e pertinente, quindi saprai se il cambiamento ha un effetto significativo sugli utenti reali in scenari pertinenti.

Le strutture consumano meno memoria heap (perché sono più piccole e più facilmente compattate, non perché sono "nello stack"). Ma la copia richiede più tempo rispetto a una copia di riferimento. Non so quali siano le tue metriche sulle prestazioni per l'utilizzo della memoria o la velocità; c'è un compromesso qui e tu sei la persona che sa cos'è.

È meglio creare questi oggetti come classe o come struttura?

Forse classe, forse struttura. Come regola generale: se l'oggetto è:
1. Piccolo
2. Logicamente un valore immutabile
3. Ce ne sono molti
Quindi prenderei in considerazione di renderlo uno struct. Altrimenti mi atterrei a un tipo di riferimento.

Se hai bisogno di mutare qualche campo di una struttura, di solito è meglio costruire un costruttore che restituisca un'intera nuova struttura con il campo impostato correttamente. Forse è leggermente più lento (misuralo!) Ma logicamente molto più facile da ragionare.

Gli oggetti nell'heap e nello stack vengono elaborati allo stesso modo dal Garbage Collector?

No , non sono la stessa cosa perché gli oggetti in pila sono le radici della collezione . Il garbage collector non ha bisogno di chiedere "questa cosa nello stack è viva?" perché la risposta a questa domanda è sempre "Sì, è in pila". (Ora, non puoi fare affidamento su questo per mantenere in vita un oggetto perché lo stack è un dettaglio di implementazione. Il jitter può introdurre ottimizzazioni che, ad esempio, registrano quello che normalmente sarebbe un valore dello stack, e quindi non è mai sullo stack quindi il GC non sa che è ancora vivo. Un oggetto registrato può avere i suoi discendenti raccolti in modo aggressivo, non appena il registro che lo tiene non verrà letto di nuovo.)

Ma il garbage collector non deve trattare oggetti in pila come vivo, allo stesso modo in cui gestisce qualsiasi oggetto noto per essere vivo come vivo. L'oggetto nello stack può fare riferimento a oggetti allocati nell'heap che devono essere mantenuti in vita, quindi il GC deve trattare gli oggetti dello stack come oggetti allocati nell'heap vivente allo scopo di determinare il set live. Ma, ovviamente, essi sono non trattati come "oggetti dal vivo" ai fini della compattazione del mucchio, perché non sono sul mucchio, in primo luogo.

È chiaro?


Eric, sai se il compilatore o il jitter fanno uso dell'immutabilità (forse se applicata con readonly) per consentire le ottimizzazioni. Non lascerei che ciò influenzi una scelta sulla mutabilità (in teoria sono un pazzo per i dettagli sull'efficienza, ma in pratica il mio primo passo verso l'efficienza è sempre cercare di avere una garanzia di correttezza il più semplice possibile e quindi non devo sprecare cicli della CPU e cicli cerebrali su controlli e casi limite, ed essere opportunamente mutabili o immutabili aiuta lì), ma contrasterebbe qualsiasi reazione istintiva al tuo dire che l'immutabilità può essere più lenta.
Jon Hanna

@ Jon: il compilatore C # ottimizza i dati const ma non i dati di sola lettura . Non so se il compilatore jit esegue ottimizzazioni della cache sui campi di sola lettura.
Eric Lippert

Un vero peccato, perché so che la conoscenza dell'immutabilità consente alcune ottimizzazioni, ma a quel punto ha raggiunto i limiti delle mie conoscenze teoriche, ma sono limiti che mi piacerebbe estendere. Nel frattempo "può essere più veloce in entrambe le direzioni, ecco perché, ora prova e scopri cosa vale in questo caso" è utile poter dire :)
Jon Hanna

Consiglierei di leggere simple-talk.com/dotnet/.net-framework/… e il tuo articolo (@Eric): blogs.msdn.com/b/ericlippert/archive/2010/09/30/… per iniziare a immergerti nei dettagli. Ci sono molti altri buoni articoli in giro. A proposito, la differenza nell'elaborazione di 100.000 piccoli oggetti in memoria è appena percettibile grazie a un certo sovraccarico di memoria (~ 2,3 MB) per classe. Può essere facilmente verificato con un semplice test.
Nick Martyshchenko

Sì, è chiaro. Grazie mille per la tua risposta esauriente (estesa è meglio? Google translate ha fornito 2 traduzioni. Volevo dire che ti sei preso il tempo per non scrivere una risposta breve, ma anche per scrivere tutti i dettagli).
Michel

23

A volte structnon è necessario chiamare il costruttore new () e assegnare direttamente i campi rendendolo molto più veloce del solito.

Esempio:

Value[] list = new Value[N];
for (int i = 0; i < N; i++)
{
    list[i].id = i;
    list[i].isValid = true;
}

è circa 2 o 3 volte più veloce di

Value[] list = new Value[N];
for (int i = 0; i < N; i++)
{
    list[i] = new Value(i, true);
}

dove Valueè a structcon due campi ( ide isValid).

struct Value
{
    int id;
    bool isValid;

    public Value(int i, bool isValid)
    {
        this.i = i;
        this.isValid = isValid;
    }
}

D'altra parte, è che gli elementi devono essere spostati o i tipi di valore selezionati, tutto ciò che la copia ti rallenterà. Per ottenere la risposta esatta sospetto che tu debba profilare il tuo codice e testarlo.


Ovviamente le cose diventano molto più veloci quando si marshalling anche i valori oltre i confini nativi.
leppie

Suggerirei di utilizzare un nome diverso da list, dato che il codice indicato non funzionerà con a List<Value>.
supercat

7

Le strutture possono sembrare simili alle classi, ma ci sono differenze importanti di cui dovresti essere a conoscenza. Prima di tutto, le classi sono tipi di riferimento e le strutture sono tipi di valore. Usando gli struct, puoi creare oggetti che si comportano come i tipi incorporati e goderti anche i loro vantaggi.

Quando chiami l'operatore New su una classe, verrà allocato nell'heap. Tuttavia, quando si crea un'istanza di una struttura, viene creata nello stack. Ciò produrrà miglioramenti delle prestazioni. Inoltre, non avrai a che fare con i riferimenti a un'istanza di una struttura come faresti con le classi. Lavorerai direttamente con l'istanza della struttura. Per questo motivo, quando si passa una struttura a un metodo, viene passata per valore anziché come riferimento.

Di più qui:

http://msdn.microsoft.com/en-us/library/aa288471(VS.71).aspx


4
So che lo dice su MSDN, ma MSDN non racconta l'intera storia. Stack vs. heap è un dettaglio di implementazione e le strutture non vengono sempre inserite nello stack. Per un solo blog recente su questo, vedere: blogs.msdn.com/b/ericlippert/archive/2010/09/30/…
Anthony Pegram

"... è passato per valore ..." sia i riferimenti che gli struct vengono passati per valore (a meno che non si usi 'ref') - è se un valore o un riferimento viene passato che differisce, cioè gli struct vengono passati valore per valore , gli oggetti classe vengono passati riferimento per valore e i parametri contrassegnati ref passano riferimento per riferimento.
Paul Ruane

10
L'articolo è fuorviante su diversi punti chiave e ho chiesto al team di MSDN di rivederlo o eliminarlo.
Eric Lippert

2
@supercat: per affrontare il tuo primo punto: il punto più grande è che nel codice gestito in cui è memorizzato un valore o un riferimento a un valore è in gran parte irrilevante . Abbiamo lavorato duramente per creare un modello di memoria che il più delle volte consenta agli sviluppatori di consentire al runtime di prendere decisioni di archiviazione intelligenti per loro conto. Queste distinzioni sono molto importanti quando la mancata comprensione delle stesse ha conseguenze negative come in C; non tanto in C #.
Eric Lippert

1
@supercat: per affrontare il tuo secondo punto, nessuna struttura mutabile è per lo più malvagia. Ad esempio, void M () {S s = new S (); s.Blah (); N (s); }. Refactoring in: void DoBlah (S s) {s.Blah (); } void M (S s = new S (); DoBlah (s); N (s);}. Questo ha appena introdotto un bug perché S è una struttura mutabile. Hai visto immediatamente il bug? O il fatto che S sia una struttura mutabile ti nasconde il bug?
Eric Lippert

6

Gli array di strutture sono rappresentati sull'heap in un blocco di memoria contiguo, mentre un array di oggetti è rappresentato come un blocco contiguo di riferimenti con gli oggetti effettivi stessi altrove sull'heap, richiedendo così memoria sia per gli oggetti che per i loro riferimenti all'array .

In questo caso, poiché li stai inserendo in a List<>(e a List<>è appoggiato su un array) sarebbe più efficiente, dal punto di vista della memoria, usare gli struct.

(Attenzione però, gli array di grandi dimensioni troveranno la loro strada nel Large Object Heap dove, se la loro durata è lunga, potrebbe avere un effetto negativo sulla gestione della memoria del processo. Ricorda, inoltre, che la memoria non è l'unica considerazione.)


Puoi utilizzare la refparola chiave per affrontare questo problema.
leppie

"Attenzione, però, che i grandi array troveranno la loro strada nel Large Object Heap dove, se la loro durata è lunga, potrebbe avere un effetto negativo sulla gestione della memoria del processo." - Non sono abbastanza sicuro del motivo per cui lo pensi? L'allocazione sulla LOH non causerà alcun effetto negativo sulla gestione della memoria a meno che (possibilmente) non sia un oggetto di breve durata e tu voglia recuperare rapidamente la memoria senza aspettare una raccolta di seconda generazione.
Jon Artus

@ Jon Artus: il LOH non si compatta. Qualsiasi oggetto di lunga durata dividerà il LOH nell'area di memoria libera prima e nell'area dopo. Per l'allocazione è richiesta una memoria contigua e se queste aree non sono abbastanza grandi per un'allocazione, viene allocata più memoria al LOH (cioè si otterrà una frammentazione LOH).
Paul Ruane

4

Se hanno una semantica dei valori, probabilmente dovresti usare una struttura. Se hanno una semantica di riferimento, probabilmente dovresti usare una classe. Ci sono eccezioni, che per lo più tendono a creare una classe anche quando ci sono semantiche di valore, ma inizia da lì.

Per quanto riguarda la seconda modifica, il GC si occupa solo dell'heap, ma c'è molto più spazio sull'heap rispetto allo spazio sullo stack, quindi mettere le cose sullo stack non è sempre una vittoria. Oltre a ciò, un elenco di tipi di struttura e un elenco di tipi di classe saranno nell'heap in entrambi i casi, quindi questo è irrilevante in questo caso.

Modificare:

Comincio a considerare dannoso il termine male . Dopo tutto, rendere mutabile una classe è una cattiva idea se non è attivamente necessario, e non escluderei mai di usare una struttura mutabile. È una cattiva idea così spesso da essere quasi sempre una cattiva idea, ma per lo più semplicemente non coincide con la semantica del valore, quindi non ha senso usare una struttura nel caso specifico.

Ci possono essere ragionevoli eccezioni con gli struct annidati privati, dove tutti gli usi di quello struct sono quindi limitati a un ambito molto limitato. Questo però non si applica qui.

In realtà, penso che "muta quindi è un cattivo stuct" non sia molto meglio che andare avanti con l'heap e lo stack (che almeno ha un certo impatto sulle prestazioni, anche se spesso travisato). "Muta, quindi è molto probabile che non abbia senso considerarlo come semantica del valore, quindi è una struttura sbagliata" è solo leggermente diverso, ma è importante che lo ritenga.


3

La soluzione migliore è misurare, misurare di nuovo, quindi misurare ancora un po '. Potrebbero esserci dettagli di ciò che stai facendo che potrebbero rendere difficile una risposta semplice e semplificata come "usa strutture" o "usa classi".


Concordo con la parte di misura, ma a mio avviso era un esempio semplice e chiaro, e ho pensato che forse si potrebbero dire alcune cose generiche al riguardo. E come si è scoperto, alcune persone lo hanno fatto.
Michel

3

Una struttura è, in fondo, niente di più né di meno che un'aggregazione di campi. In .NET è possibile che una struttura "finga" di essere un oggetto, e per ogni tipo di struttura .NET definisce implicitamente un tipo di oggetto heap con gli stessi campi e metodi che, essendo un oggetto heap, si comporteranno come un oggetto . Una variabile che contiene un riferimento a tale oggetto heap (struttura "boxed") mostrerà la semantica di riferimento, ma quella che contiene direttamente una struttura è semplicemente un'aggregazione di variabili.

Penso che gran parte della confusione tra struttura e classe derivi dal fatto che le strutture hanno due casi di utilizzo molto diversi, che dovrebbero avere linee guida di progettazione molto diverse, ma le linee guida MS non fanno distinzione tra di loro. A volte c'è bisogno di qualcosa che si comporti come un oggetto; in tal caso, le linee guida MS sono abbastanza ragionevoli, sebbene il "limite di 16 byte" dovrebbe probabilmente essere più simile a 24-32. A volte, tuttavia, ciò che serve è un'aggregazione di variabili. Una struttura usata a tale scopo dovrebbe semplicemente consistere in un gruppo di campi pubblici e possibilmente un Equalsoverride, ToStringoverride eIEquatable(itsType).Equalsimplementazione. Le strutture usate come aggregazioni di campi non sono oggetti e non dovrebbero fingere di esserlo. Dal punto di vista della struttura, il significato di campo non dovrebbe essere né più né meno che "l'ultima cosa scritta in questo campo". Qualsiasi significato aggiuntivo dovrebbe essere determinato dal codice client.

Ad esempio, se una struttura di aggregazione di variabili ha membri Minimume Maximum, la struttura stessa non dovrebbe prometterlo Minimum <= Maximum. Il codice che riceve una tale struttura come parametro dovrebbe comportarsi come se fosse passato separatamente Minimume Maximumvalori. Un requisito che Minimumnon sia maggiore di Maximumdovrebbe essere considerato come un requisito che un Minimumparametro non sia maggiore di uno passato separatamente Maximum.

Un modello utile da considerare a volte è avere una ExposedHolder<T>classe definita qualcosa come:

class ExposedHolder<T>
{
  public T Value;
  ExposedHolder() { }
  ExposedHolder(T val) { Value = T; }
}

Se uno ha una List<ExposedHolder<someStruct>>, in cui someStructè una struttura variabile aggregazione, uno può fare le cose come myList[3].Value.someField += 7;, ma dando myList[3].Valuead altro codice darà il contenuto di Valuepiuttosto che dare è un mezzo di alterarlo. Al contrario, se si usasse a List<someStruct>, sarebbe necessario usare var temp=myList[3]; temp.someField += 7; myList[3] = temp;. Se si utilizza un tipo di classe mutabile, l'esposizione del contenuto di myList[3]al codice esterno richiederebbe la copia di tutti i campi su qualche altro oggetto. Se si usasse un tipo di classe immutabile, o una struttura "in stile oggetto", sarebbe necessario costruire una nuova istanza che fosse simile, myList[3]tranne quella someFielddiversa, e quindi memorizzare quella nuova istanza nell'elenco.

Una nota aggiuntiva: se si memorizza un gran numero di cose simili, potrebbe essere utile memorizzarle in matrici di strutture possibilmente annidate, preferibilmente cercando di mantenere la dimensione di ogni matrice tra 1K e 64K circa. Le matrici di strutture sono speciali, in quanto l'indicizzazione produrrà un riferimento diretto a una struttura all'interno, quindi si può dire "a [12] .x = 5;". Sebbene sia possibile definire oggetti simili ad array, C # non consente loro di condividere tale sintassi con gli array.


1

Usa le lezioni.

In una nota generale. Perché non aggiornare il valore b mentre li crei?


1

Da una prospettiva c ++ sono d'accordo che sarà più lento modificare le proprietà di una struttura rispetto a una classe. Ma penso che saranno più veloci da leggere a causa della struttura allocata sullo stack invece che sull'heap. La lettura dei dati dall'heap richiede più controlli che dallo stack.


1

Bene, se vai con la struttura dopotutto, elimina la stringa e usa il carattere di dimensione fissa o il buffer di byte.

Questo è re: 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.