Continuiamo con le speculazioni estive intorno all'argomento del boxing/unboxing e perfomance.

Nel post precedente abbiamo parlato di cosa vogliano dire i termini in oggetto, e abbiamo evidenziate le motivazioni che stanno dietro alle penalità di perfomance proposte per queste attività.

Qui ora acceneremo qualcosa riguardo ai generics.

 Uno dei grossi vantaggi introdotti dal mondo "generics" è legato alle perfomance, oltre che la "type safety".

L'abbiamo già accennato, ma è meglio rispecificare. Con quest'ultimo termine si intendono le attività atte a evitare gli errori derivanti da lavorare con variabili di tipo incerto: la definizione è abbastanza generica, ma sono certo che rende bene l'idea.

Per vedere bene i vantaggi e gli svantaggi vediamo l’utilizzo della classe ArrayList fornita da .Net framework, e che è una delle poche classi che permette l’utilizzo di object come argomento.

Per i pochi che non lo sanno la classe ArrayList rappresenta un semplice array di object la cui dimensione autonomamente e dinamicamente si adatta ai valori inseriti senza dover fare alcunchè.

Essendo che tutte le variabili in .Net derivano da System.Object, sia direttamente che indirettamente, ecci che quindi object è in grado di ospitare qualisiasi tipo di dato.

Può sembrare una stranezza, ma è proprio così: usando ArrayList è possibile aggiungere a questa un intero, una stringa, un oggetto, o comunque una variabile di qualsiasi tipo ci passi per la testa.

Questa "libertà", però, ha conseguenze nefaste, se non è ben gestita.

 Il semplice snippet nel seguito evidenzia le caratteristiche principali dell’oggetto ArrayList.

ArrayList arrlist = new ArrayList();
arrlist.Add("One");
arrlist.Add("Two");
arrlist.Add("Three");
arrlist.Add(4); //Boxing arrlist.RemoveAt(1); // rimuove il primo elemento string value = arrlist[i] as string; // Si ottiene l’iesimo elemento
int valore = (int)arrlist[4]; //UnBoxing
arrlist.Sort(); //====>Errore !

Ora occorre ragionare un attimo su come gli elementi vengono memorizzati.

Essendo che la lista lavora su reference type (infatti è un object) quando si esegue un metodo Add dell'oggetto ArrayList per esempio per aggiungere un intero, si sta eseguendo una conversione da un oggetto posto nello stack in un altro oggetto posto nell’heap: questa possibilità è anch'essa un boxing.

Come visto nel post precedente con il termine boxing si intende qualsiasi attività di porre un valore primitivo nell'heap: in altri termini è l'attività di porre un valore che naturalmente sarebbe un value type in un reference type.

Viene usato il termine boxing perchè il valore intero viene messo in una scatola (un oggetto) e quindi messo nell'heap. In questa area di memoria non sarebbe possibile porre dati primitivi, ma solo oggetti, e per tale motivo prima di inserire un intero questo deve essere inscatolato in un oggetto che viene creato al volo per contenere il valore.

La procedura inversa, che nel caso sopra è data dalla seguente istruzione, è chiamata unboxing.

E' possibile affermare che si ha un unboxing tutte le volte in cui occorre fare un cast. Quindi si preleva l'oggetto e si tira fuori il valore primitivo lì memorizzato.

Le operazioni di boxing e unboxing sono molto onerose sotto il punto di vista computazionale: anzi a essere precisi sono tra le operazioni più onerose che esistano in .Net framework.

Inoltre è evidente che l’utilizzo di questo tipo di oggetto porti a problemi di type safety: mettere all’interno di ArrayList tipi di qualsiasi genere può portare a problemi.

Per esempio nel caso sopra quando si estrae un elemento questo può essere un qualsiasi tipo: inoltre se si tenta di usare, ad esempio, Sort si avrà un errore perché .NET non saprebbe come ordinare elementi non omogenei.

Esistono in C# diverse classi che supportano liste di elementi tipizzati, e quindi non soffrono dei problemi esposti sopra: una di queste, che in pratica è la versione generics di ArrayList, è la classe List.

Esemplificando al massimo la classe List fa tutto quello che fa ArrayList, solo che è generics, per cui abbisogna, nella sua dichiarazione, di passare il tipo su cui lavora.

La differenza di perfomance utilizzando le due liste viene evidenziato dal semplice listato di seguito.

DateTime m_DataOraInizio = DateTime.Now;
ArrayList listArray = new ArrayList();
for (int i = 0; i < 10000; i++)
{
    listArray.Add(i);
}

foreach (int item in listArray)
{
    Console.WriteLine(item);
}

DateTime m_DataOraFine = DateTime.Now;
Console.WriteLine("Secondi impiegati per ArrayList: " + m_DataOraFine.Subtract(m_DataOraInizio).TotalSeconds.ToString());

m_DataOraInizio = DateTime.Now;
List listList = new List();
for (int i = 0; i < 10000; i++)
{
    listList.Add(i);
}

foreach (int item in listList)
{
    Console.WriteLine(item);
}
m_DataOraFine = DateTime.Now;
Console.WriteLine("Secondi impiegati per List: " + m_DataOraFine.Subtract(m_DataOraInizio).TotalSeconds.ToString());

Console.ReadLine();

Usando List non si ha più boxing/unboxing, e quindi l'accesso ai dati memorizzati, risulta essere molto più perfomante.

Ma come fa List a lavorare senza usare l'heap pur avendo un dimensione arbitraria ?? Semplice: List lavora esclusivamente nello stack utilizzando una matrice tipizzata.

Mi permetto di dare una cornice all'osservazione sopra. Essendo che List può essere di dimensione arbitraria, ancorpiù è molto facile aggiungere nuovi elementi semplicemente usando, come per ArrayList, l'istruzione Add, si potrebbe pensare che venga memorizzato nell'heap.

Invece la List è memorizzata come se fosse una matrice nello stack. Per gestire eventuali nuovi inserimenti questa matrice in realtà viene creata di dimensioni decise da .Net, e quindi i nuovi inserimenti impegnano posizioni libere.

Solo all'esaurirsi di queste la matrice viene ridimensionata e vengono aggiunte nuove posizioni. Tutto questo avviene in modo assolutamente trasparente senza che ci si debba preoccupare di nulla.

Per questo motivo quando si vuole ottimizzare una List esistono le seguenti istruzioni, che possono essere usate per "spremere" al massimo le perfomance del nostro codice.

intList.Capacity = 20;
intList.TrimExcess();

Con Capacity si dichiara che non usaremo più posizioni che quelle dichiarate (nel nostro caso 20) e quindi la dimensione di questa matrice che sottende la List viene dimensionata di conseguenza.

Invece con TrimExcess si dichiara che alla List non verrà più aggiunto alcun nuovo elemento, e quindi la matrice può essere ridimensionata correttamente, rilasciando lo spazio libero allocato da .Net in fase di dichiarazione e che quindi è inutilizzato.

Conoscevate l'esistenza di queste istruzioni ?? Io francamente me lo ero perso.......

Altra osservazione: è da pazzi usare la lista nel seguente modo.

List<object>

In questo modo qualsiasi elemento posto nella lista viene memorizzato nel maganed heap, e nella relativa posizione (nello stack) viene sempicemente memorizzato un puntatore a questo elemento. Per tale motivo ogni elemento trattato sarà oggetto di boxing/unboxing.

Spero che questo cazzeggio estivo Vi abbia dato spunti interessanti.