CLR

Windows Embedded Compact 2013 : firmiamo un assembly per il .Net Compact Framework 3.9 da includere nella GAC

Con questo post, inizia un breve tutorial sull’utilizzo di una libreria proprietaria (da noi sviluppata con .Net Compact Framework 3.9) su Windows Embedded Compact 2013, dalla firma dell’assembly (per la registrazione nella GAC) fino al progetto di esempio, passando per la realizzazione di un componente esposto dal Platform Builder (nel catalog items) e da poter includere banalmente nell’immagine del sistema operativo.

In particolare vedremo :

  1. Come eseguire la firma di un assembly;
  2. Creazione di un componente proprietario per il catalog items di Platform Builder;
  3. Distribuzione ed inclusione del componente nell’immagine del sistema operativo;
  4. Supporto per il debugging dell’applicazione che utilizzerà il componente;
  5. Realizzazione di una semplice applicazione di esempio;

Prenderò come riferimento il mio progetto M2Mqtt (MQTT Client library for .Net), supponendo che sia esso il componente da voler includere nell’immagine, in modo che terze parti possano sviluppare un’applicazione che lo utilizzi avendolo già onboard su un target device con Windows Embedded Compact 2013 e .Net Compact Framework 3.9.

La firma : GAC, Public Key Token e Strong Name

Molto spesso si pone la necessità di includere un proprio assembly (magari una libreria da noi sviluppata) direttamente in un’immagine di Windows Embedded Compact, così come viene incluso il .Net Compact Framework grazie al Platform Builder (utilizzando semplicemente il catalog items). In questo modo, la DLL si troverà nella cartella \Windows del target device (insieme a tutti gli assembly del framework) e sarà utilizzabile da applicazione terze parti solo registrandola nella GAC (Global Assembly Cache).

Nel caso del .Net Compact Framework, per eseguire la verifica dell’assembly al suo caricamento dalla GAC, è necessario che esso sia firmato ed abbia un Public Key Token associato. Il Public Key Token è un hash a 64 bit della chiave pubblica relativa alla chiave privata utilizzata per firmare l’assembly. E’ utile per rendere un assembly univoco in modo che due assembly con lo stesso nome vengano considerati distinti (ecco perché si parla di Strong Name). Considerando come riferimento la mia libreria M2Mqtt per il .Net Compact Framework 3.9, vediamo quali sono i passaggi necessari per la firma dell’assembly corrispondente.

Usiamo lo Strong Name Tool

In primo luogo è necessario generare una coppia di chiavi privata/pubblica e per questa operazione possiamo utilizzare lo Strong Name Tool (sn.exe) fornito con Visual Studio. Lanciamo il “Developer Command Prompt for VS2012” ed eseguiamo il seguente comando :

sn –k M2Mqtt.snk

Firmiamo l’assembly

Il passo successivo è quello di utilizzare questo file .snk per la firma dell’assembly in modo da generare un Public Key Token corrispondente. Generalmente, un progetto basato su .Net Framework ha la tab “Signing” nella finestra delle “Properties” in cui è possibile selezionare il file.

8712.01_thumb_3FCD4E6E

Nel caso di un progetto per il .Net Compact Framework 3.9, questa tab non è disponibile ma è necessario intervenire sul file AssemblyInfo.cs specificando al suo interno la linea seguente :

[assembly:AssemblyKeyFileAttribute(“M2Mqtt.snk”)]

Il file .snk va copiato nella stessa cartella in cui si trova quello di progetto (.csproj).

Attraverso questa impostazione, al termine della compilazione avremo un assembly firmato (in questo caso M2Mqtt.dll) che possiamo ispezionare utilizzando un tool come il Reflector della RedGate.

1374.02_thumb_1826D244

Abbiamo l’assembly firmato e pronto per essere incluso nell’immagine del sistema operativo. Nel post successivo vedremo come sia possibile realizzazione un componente visibile nel catalog items di Platform Builder che includa il nostro file e tutta la relativa configurazione.

Sviluppo in .Net su processori ARM

Vi segnalo questo interessantissimo articolo “.Net Development for ARM processors” di Andrew Pardoe (Program Manager nel CLR team) su MSDN Magazine che esplora l’evoluzione dello sviluppo in .Net dal processore x86 al processore ARM, con l’introduzione del .Net Compact Framework ed il .Net Micro Framework fino all’avvento della piattaforma in Windows 8.

Sono evidenziate le notevoli differenza tra le due architetture di cui deve tener conto il team che sviluppa il CLR in modo che il passaggio da un’architettura all’altra sia trasparente per noi sviluppatori.

L’ho trovato molto…molto…interessante….leggere per credere !

Codice Nativo vs Gestito : performance

Affrontando un problema lavorativo riguardo le perfomance grafiche su un target device con processore ARM e Windows CE 6.0 R3 e facendo delle ricerche di approfondimento in rete, mi sono imbattuto in un interessantissimo articolo di Chris Tacke dell’OpenNETCF Communty intitolato “Native vs. Managed Code : GDI Performance”.

L’articolo, attraverso un’applicazione di test, dimostra quanto il codice nativo non possa essere considerato in assoluto molto più veloce del codice gestito ma soltanto per una percentuale relativa che però può talvolta variare da piattaforma a piattaforma. In particolare, il test viene eseguito utilizzando le GDI di Windows.

Un ulteriore interessante risultato è che l’utilizzo di metodi del .Net Compact Framework che fungono da wrapper di corrispondenti metodi nativi (vedi ad esempio Graphics.FillRectangleche è wrapper dell’API FillRect) riducono le performance rispetto all’invocazione diretta delle API di sistema attraverso le P/Invoke. E’ ovvio che il wrapper al suo interno eseguirà sempre una P/Invoke ma aggiunge un carico di lavoro in più, probabilmente legato a dei check che vengono eseguiti prima della chiamata nativa (vedi anche il marshalling dei parametri).

CLR – Thread Pool : elaborazioni periodiche con i timer

Capita spesso di dover eseguire periodicamente un’elaborazione ad intervalli prefissati. Una prima soluzione, assolutamente non efficace, potrebbe essere quella di predisporre un thread che al termine dell’elaborazione stessa va in sleep per un tempo pari al periodo richiesto, per poi rieseguire la computazione; tutto ciò in un opportuno ciclo. Considerando quanto detto una semplice idea e comunque un approccio non ottimale, la migliore soluzione la possiamo ottenere attraverso la classe Timer (del namespace System.Threading) che fa ampio uso del Thread Pool.

Considerando il costruttore più ampio di tale classe, esso prevede i seguenti parametri :

  • una delegate del tipo TimerCallback che rappresenta la callback da invocare ogni qual volta scatta il timer;
  • un oggetto con eventuali informazioni di stato da passare alla callback ogni qual volta viene invocata;
  • il dueTime, ossia il ritardo con il quale la callback viene invocata per la prima volta (specificando il valore 0, la callback viene invocata subito);
  • il periodo di invocazione della callback. Specificando Timeout.Infinite, viene disabilitata la periodicità e la callback sarà invocata solo una volta;

L’utilizzo di questa classe è strettamente legato al Thread Pool, in quanto il CLR utilizza uno dei worker thread del pool per invocare la callback associata al timer stesso. Ciò vuol dire che per più timer, impostati con periodi diversi e comunque aventi elaborazioni che non si sovrappongono nel tempo, il CLR è in grado di riciclare il medesimo thread per eseguire le corrispondenti callback con un notevole risparmio di risorse. E’ altresì ovvio che, qualora le elaborazioni delle callback si “sovrapponessero” nel tempo, il CLR sarebbe costretto ad allocare thread differenti per poterle eseguire.

Consideriamo la seguente semplice applicazione :

class Program
{

    // numero worker threads e completion port threads
    private static int workerThreads, completionPortThreads;

    private const int SCHEDULING_DELAY = 1000;    // delay avvio tra un thread e l'altro
    private const int NR_TIMER = 4;    // numero di thread
    private const long NR_DATA = 100000000;    // numero di elaborazioni

    private static Stopwatch sw;

    static void Main(string[] args)
    {
        Timer[] timers = new Timer[NR_TIMER];
        sw = new Stopwatch();

        WriteAvailableThreads();
        sw.Start();

        for (int idx = 0; idx < NR_TIMER; idx++)
        {
            timers[idx] = new Timer(TimerWork, idx, 5000, 1000);
            Thread.Sleep(SCHEDULING_DELAY);
        }
        Thread.Sleep(30000);

        for (int idx = 0; idx < NR_TIMER; idx++)
        {
            timers[idx].Dispose();
        }

        sw.Stop();
        Console.WriteLine("All works finished, elapsed time = {0} ms", sw.ElapsedMilliseconds);
        WriteAvailableThreads();

        Console.ReadLine();
    }

    private static void TimerWork(object state)
    {
        Console.WriteLine("Start TimerWork{0} at {1} ms with ThreadId {2}", state, sw.ElapsedMilliseconds, Thread.CurrentThread.ManagedThreadId);
        WriteAvailableThreads();

        long sum = 0;
        for (int i = 0; i < NR_DATA; i++)
        {
            sum += i;
        }

        Console.WriteLine("End TimerWork{0} at {1} ms with ThreadId {2}", state, sw.ElapsedMilliseconds, Thread.CurrentThread.ManagedThreadId);
    }

    private static void WriteAvailableThreads()
    {
        ThreadPool.GetAvailableThreads(out workerThreads, out completionPortThreads);
        Console.WriteLine("workerthreads = {0}, completionPortThreads = {1}", workerThreads, completionPortThreads);
    }
}

Attraverso la quale creiamo NR_TIMER timer con un delay l’uno rispetto l’altro pari a SCHEDULING_DELAY e supponiamo che la callback associata ai timer esegua un’elaborazione (una banalissima somma) con un certo numero di dati pari a NR_DATA. Assumiamo che il dueTime sia 5000 ms e che il timer scatti una sola volta e non periodicamente.

Di seguito un primo risultato.

2376.timer1_34E65D33

SCHEDULING_DELAY = 1000, NR_TIMER = 4, NR_DATA = 10000000

Si osserva che schedulando abbastanza lentamente l’avvio dei timer, allo scattare delle relative callback e considerando la velocità dell’elaborazione, il CLR riesce a riciclare sempre il medesimo worker thread.

Proviamo a velocizzare la schedulazione dei timer.

4401.timer2_60B6B122

SCHEDULING_DELAY = 10, NR_TIMER = 4, NR_DATA = 10000000

In questo caso, il CLR è costretto ad utilizzare due worker thread per eseguire le callback previste per i quattro timer.

Aggiungendo la periodicità al timer (in questo caso l’ho settata a 1000 ms), la situazione diventa non molto predicibile e complessa. Dallo screenshot che segue si evince come ad un certo punto il CLR abbia avuto la necessità di allocare 6 worker thread (da 1023 disponibili a 1017) per gestire 4 timer. Ciò è dovuto al fatto che, quando un worker thread è occupato e non ce ne sono altri già creati e disponibili, il CLR è costretto ad allocarne uno nuovo e che quelli già presenti nel pool vengono eliminati, se non usati, solo dopo un certo tempo di inattività.

3443.timer3_58BF0EC0

SCHEDULING_DELAY = 1000, NR_TIMER = 4, NR_DATA = 10000000, PERIOD = 1000

CLR – Thread Pool : attesa di eventi con i wait handlers

Capita spesso di dover eseguire una certa elaborazione al verificarsi di un evento. Ovviamente, non faccio riferimento agli eventi scatenati sulla UI (es. click di un bottone,..) ai quali la gestione è demandata ai relativi event handlers, ma mi riferisco ad eventi intrinseci dell’applicazione che magari servono per sincronizzare uno o più thread fra loro (es. sbloccare un thread nel momento in cui un altro thread ha terminato una computazione ed ha reso disponibili dei dati).

La soluzione tipica è quella di creare un WaitHandle (concretamente un AutoResetEvent oManualResetEvent in base alle esigenze) ed un thread, dopodichè bloccare quest’ultimo mediante una WaitOne() sull’evento stesso. In questo modo, il thread va in wait e si sbloccherà nel momento in cui “qualcuno” (un altro thread o il main thread) eseguirà unaSet() sull’evento. Questa tecnica, prevede comunque la creazione di un thread custom da parte dello sviluppatore.

Ebbene, anche in una situazione di questo tipo, ci viene fornito supporto dal thread pool del CLR. Infatti, utilizzando il metodo statico RegisterWaitForSingleObject() della classeThreadPool, è possibile demandare al CLR il monitoring di un evento e far eseguire una callback nel caso in cui quest’ultimo diventi signaled o nel caso in cui scatti un timeout.

Il metodo RegisterWaitForSingleObject prevede i seguenti parametri :

  • un WaitHandle, ossia l’evento da monitorare;
  • la callback da eseguire nel momento in cui l’evento viene segnalato o scatta il timeout. La callaback è definita attraverso il delegate WaitOrTimerCallback;
  • un oggetto di stato da passare alla callback (utile per trasferire dati a quest’ultima);
  • un timeout in millisecondi allo scattare del quale la callback viene comunque invocata nonostante l’evento non sia stato segnalato;
  • una flag che indica se il monitoring dell’evento deve essere ripetuto nel tempo oppure se va eseguito una sola volta (nel cui caso al signaling dell’evento o alla scattare del timeout, l’evento non verrà più monitorato);

Infine, esso ritorna un RegisteredWaitHandle sul quale va eseguito il metodo diUnregister() nel momento in cui non è più necessario, in modo da liberare le risorse allocate per l’evento. E’ da osservare che il delegate WaitOrTimerCallback prevede un primo parametro che rappresenta lo stato eventualmente passato alla callback ed un secondo parametro booleano che indica se la callback è stata chiamata per timeout o meno.

Consideriamo la seguente applicazione console :

class Program
{
    // numero worker threads e completion port threads
    private static int workerThreads, completionPortThreads;

    private const int SCHEDULING_DELAY = 1000;
    private const int NR_WAIT_CALLBACK = 2;
    private const long NR_DATA = 100000000;
    private const int WAIT_TIMEOUT = 5000;
    private const int SET_DELAY = 1000;

    private static AutoResetEvent[] waitEvent;              // wait handles da monitorare
    private static AutoResetEvent[] syncEvent;              // wait handles per la sincronizzazione con il main thread
    private static RegisteredWaitHandle[] regWaitHandle;    // handle relativi alle risorse allocate dal manager
                                                            // del thread pool per il monitoring
    private static Stopwatch sw;

    static void Main(string[] args)
    {
        waitEvent = new AutoResetEvent[NR_WAIT_CALLBACK];
        syncEvent = new AutoResetEvent[NR_WAIT_CALLBACK];
        regWaitHandle = new RegisteredWaitHandle[NR_WAIT_CALLBACK];
        sw = new Stopwatch();

        WriteAvailableThreads();
        sw.Start();

        Console.WriteLine("Generating wait event...");
        for (int idx = 0; idx < NR_WAIT_CALLBACK; idx++)
        {
            waitEvent[idx] = new AutoResetEvent(false);
            syncEvent[idx] = new AutoResetEvent(false);
            regWaitHandle[idx] = ThreadPool.RegisterWaitForSingleObject(waitEvent[idx], WaitMethod, idx, WAIT_TIMEOUT, true);
            Thread.Sleep(SCHEDULING_DELAY);
        }

        Console.WriteLine("Waiting for all wait methods...");
        WaitHandle.WaitAll(syncEvent);

        sw.Stop();
        Console.WriteLine("All  wait methods finished, elapsed time = {0} ms", sw.ElapsedMilliseconds);

        for (int j = 0; j < NR_WAIT_CALLBACK; j++)
        {
            regWaitHandle[j].Unregister(waitEvent[j]);
        }

        Console.ReadLine();
    }

    private static void WriteAvailableThreads()
    {
        ThreadPool.GetAvailableThreads(out workerThreads, out completionPortThreads);
        Console.WriteLine("workerthreads = {0}, completionPortThreads = {1}", workerThreads, completionPortThreads);
    }

    private static void WaitMethod(object state, bool timeout)
    {
        int waitMethodIdx = (int)state;
        Console.WriteLine("Start WaitMethod{0} with timeout={1} at {2} ms with ThreadId {3}", waitMethodIdx + 1, timeout, sw.ElapsedMilliseconds, Thread.CurrentThread.ManagedThreadId);
        WriteAvailableThreads();

        int sum = 0;
        for (int i = 0; i < NR_DATA; i++)
        {
            sum += i;
        }
        Console.WriteLine("End WaitMethod{0} at {1} ms with ThreadId {2}", waitMethodIdx + 1, sw.ElapsedMilliseconds, Thread.CurrentThread.ManagedThreadId);
        syncEvent[waitMethodIdx].Set();
    }
}

In cui :

  • waitEvent è l’array con i wait handle da monitorare e sui quali saremo in attesa;
  • syncEvent  è l’array con i wait handle per la sincronizzazione tra il main thread ed i thread relativi alle callback eseguite in seguito allo scattare degli eventi o del timeout;
  • regWaitHandle è l’array con i RegisteredWaitHandle usati dal manager del thread pool per gestire il monitoring;

L’applicativo non fa nient’altro che registrare NR_WAIT_CALLBACK in attesa sugli eventi dell’array waitEvent con un tempo di schedulazione di SCHEDULING_DELAY millisecondi. Inoltre, è definito un timeout pari a WAIT_TIMEOUT millisecondi. La prima versione del codice applicativo, prevede che le callback si sblocchino tutte per timeout. Infine, NR_DATA servirà per simulare un’elaborazione nelle callback.

Proviamo ad eseguirla con i seguenti valori :

  • SCHEDULING_DELAY = 100;
  • NR_WAIT_CALLBACK = 2;
  • NR_DATA = 100000000;
  • WAIT_TIMEOUT = 5000;

Otteniamo il seguente output :

1805.wait_handle_1_1B282EDF

SCHEDULING_DELAY = 100, NR_WAIT_CALLBACK = 2, WAIT_TIMEOUT = 5000

In questo caso, la registrazione di wait sull’handle avviene rapidamente (100 ms di delay) e si osserva che, allo scattare del timeout (5 sec) ovviamente le due callback dovranno essere chiamate “quasi” in contemporanea, considerando anche il tempo abbastanza lungo di elaborazione per ciascuna di esse. Per questo motivo, vengono allocati due completion port thread per eseguire tali callback.

Proviamo ad aumentare il delay tra una richiesta di registrazione e l’altra. Otteniamo …

8741.wait_handle_2_78F02358

SCHEDULING_DELAY = 1000, NR_WAIT_CALLBACK = 2, WAIT_TIMEOUT = 5000

In questo caso, la prima callback verrà invocata un secondo prima della seconda callback e considerato il fatto che l’elaborazione termina entro tale secondo, il manager del thread pool riesce a riciclare il medesimo completion port thread per gestirle entrambe. Se proviamo, però, ad aumentare il tempo di elaborazione (portando NR_DATA a 1000000000), la prima callback non sarà ancora terminata quando scatterà il timeout per la seconda, per cui il manager del thread pool sarà costretto ad allocare un nuovo thread.

8154.wait_handle_3_5F1BED29

SCHEDULING_DELAY = 1000, NR_WAIT_CALLBACK = 2, WAIT_TIMEOUT = 5000, NR_DATA = 1000000000

Aggiungiamo ora la condizione di sblocco settando gli eventi. Per fare questo, inseriamo le seguenti righe di codice subito dopo il ciclo di registrazione delle callback :

for (int k = 0; k < NR_WAIT_CALLBACK; k++)
{
    waitEvent[k].Set();
    Thread.Sleep(SET_DELAY);
}

Tra un’operazione di signaling di un evento e la successiva, lasciamo trascorrere un certo tempo per simulare uno sblocco differito. Ovviamente, modulando questo tempo, verrà riciclato o meno lo stesso thread del pool per eseguire la callback :

  • con SET_DELAY piccolo, i signaling sono così ravvicinati e magari l’elaborazione nella callback abbastanza lunga da costringere il CLR ad allocare thread separati nel pool;
  • con SET_DELAY grande, considerando il medesimo tempo di elaborazione nella callback, passa comunque più tempo tra un signaling ed il successivo per cui il CLR ha il tempo di riciclare il medesimo thread;

Riporto di seguito due possibili situazioni

1513.wait_handle_4_733509B2

SET_DELAY=100

0825.wait_handle_5_446F5110

SET_DELAY=1000

CLR – Thread Pool : uso dei worker thread

Una delle possibilità di utilizzo del thread pool è quella di usufruire dei thread al suo interno per poter eseguire in parallelo delle operazione CPU bound, ossia di calcolo puro. Ovviamente, il CLR definisce una dimensione massima del pool, sia in termini di worker thread che di completion port thread. Per quanto concerne i worker thread, i limiti di default sono i seguenti :

  • 1023 nel .Net Framework 4.0 (ambiente a 32 bit);
  • 32768 nel .Net Framework 4.0 (ambiente a 64 bit);
  • 250 per core nel .Net Framework 3.5;
  • 25 per core nel .Net Framework 2.0;

Tali valori possono essere modificati al ribasso, utilizzando il metodo staticoThreadPool.SetMaxThreads, così come è possibile definirne il numero minimo medianteThreadPool.SetMinThreads. Invece, il numero di thread disponibili nel pool in un dato momento è restituito da ThreadPool.GetAvailableThreads.

Partiamo con una semplice analisi, considerando il seguente applicativo console :

<br />class Program<br />{<br /><%%KEEPWHITESPACE%%>    // numero worker threads e completion port threads<br /><%%KEEPWHITESPACE%%>    private static int workerThreads, completionPortThreads;<br /><br /><%%KEEPWHITESPACE%%>    private const int SCHEDULING_DELAY = 1000;    // delay avvio tra un thread e l'altro<br /><%%KEEPWHITESPACE%%>    private const int NR_WORKER_THREADS = 2;    // numero di thread<br /><%%KEEPWHITESPACE%%>    private const long NR_DATA = 100000000;    // numero di elaborazioni<br /><br /><%%KEEPWHITESPACE%%>    private static int[] inputData;<br /><%%KEEPWHITESPACE%%>    private static int[] outputData;<br /><%%KEEPWHITESPACE%%>    private static ManualResetEvent[] resetEvent;<br /><%%KEEPWHITESPACE%%>    private static Stopwatch sw;<br /><br /><%%KEEPWHITESPACE%%>    static void Main(string[] args)<br /><%%KEEPWHITESPACE%%>    {<br /><%%KEEPWHITESPACE%%>        inputData = new int[NR_DATA];<br /><%%KEEPWHITESPACE%%>        outputData = new int[NR_DATA];<br /><%%KEEPWHITESPACE%%>        resetEvent = new ManualResetEvent[NR_WORKER_THREADS];<br /><%%KEEPWHITESPACE%%>        sw = new Stopwatch();<br /><br /><%%KEEPWHITESPACE%%>        Console.WriteLine("Generating random data...");<br /><%%KEEPWHITESPACE%%>        Random rand = new Random();<br /><%%KEEPWHITESPACE%%>        for (int idx = 0; idx &lt; NR_DATA; idx++)<br /><%%KEEPWHITESPACE%%>        {<br /><%%KEEPWHITESPACE%%>            inputData[idx] = rand.Next(10);<br /><%%KEEPWHITESPACE%%>        }<br /><br /><%%KEEPWHITESPACE%%>        WriteAvailableThreads();<br /><%%KEEPWHITESPACE%%>        sw.Start();<br /><br /><%%KEEPWHITESPACE%%>        Console.WriteLine("Start scheduling works...");<br /><%%KEEPWHITESPACE%%>        for (int j = 0; j &lt; NR_WORKER_THREADS; j++)<br /><%%KEEPWHITESPACE%%>        {<br /><%%KEEPWHITESPACE%%>            resetEvent[j] = new ManualResetEvent(false);<br /><%%KEEPWHITESPACE%%>            ThreadPool.QueueUserWorkItem(DoWork, j);<br /><%%KEEPWHITESPACE%%>            Thread.Sleep(SCHEDULING_DELAY);<br /><%%KEEPWHITESPACE%%>        }<br /><%%KEEPWHITESPACE%%>        Console.WriteLine("Waiting for all works...");<br /><%%KEEPWHITESPACE%%>        WaitHandle.WaitAll(resetEvent);<br /><%%KEEPWHITESPACE%%>        sw.Stop();<br /><%%KEEPWHITESPACE%%>        Console.WriteLine("All works finished, elapsed time = {0} ms", sw.ElapsedMilliseconds);<br /><br /><%%KEEPWHITESPACE%%>        Console.ReadLine();<br /><%%KEEPWHITESPACE%%>    }<br /><br /><%%KEEPWHITESPACE%%>    private static void DoWork(object state)<br /><%%KEEPWHITESPACE%%>    {<br /><%%KEEPWHITESPACE%%>        int workIdx = (int)state;<br /><%%KEEPWHITESPACE%%>        Console.WriteLine("Start DoWork{0} at {1} ms with ThreadId {2}", workIdx + 1, sw.ElapsedMilliseconds, Thread.CurrentThread.ManagedThreadId);<br /><%%KEEPWHITESPACE%%>        WriteAvailableThreads();<br /><br /><%%KEEPWHITESPACE%%>        for (int idx = 0; idx &lt; NR_DATA; idx++)<br /><%%KEEPWHITESPACE%%>        {<br /><%%KEEPWHITESPACE%%>            outputData[workIdx] += inputData[idx] * inputData[idx];<br /><%%KEEPWHITESPACE%%>        }<br /><br /><%%KEEPWHITESPACE%%>        Console.WriteLine("End DoWork{0} at {1} ms with ThreadId {2}", workIdx + 1, sw.ElapsedMilliseconds, Thread.CurrentThread.ManagedThreadId);<br /><%%KEEPWHITESPACE%%>        resetEvent[workIdx].Set();<br /><%%KEEPWHITESPACE%%>    }<br /><br /><%%KEEPWHITESPACE%%>    private static void WriteAvailableThreads()<br /><%%KEEPWHITESPACE%%>    {<br /><%%KEEPWHITESPACE%%>        ThreadPool.GetAvailableThreads(out workerThreads, out completionPortThreads);<br /><%%KEEPWHITESPACE%%>        Console.WriteLine("workerthreads = {0}, completionPortThreads = {1}", workerThreads, completionPortThreads);<br /><%%KEEPWHITESPACE%%>    }<br />}<br />

di cui abbiamo :

  • un metodo DoWork che simula un’elaborazione CPU bound, eseguendo una semplice computazione sugli elementi dell’array inputData, la cui dimensione è fissata mediante la costante NR_DATA;
  • il Main che ha il compito di lanciare/schedulare un certo numero di elaborazioni parallele eseguite con il medesimo metodo DoWork; il numero è fissato mediante la costante NR_WORKER_THREADS e il delay tra un avvio e l’altro mediante laSCHEDULING_DELAY;
  • l’array resetEvent di ManualResetEvent che serve per la sincronizzazione tra il thread principale e i thread che via via terminano;

Per quanto riguarda l’output, l’applicativo visualizza sulla console i tempi di avvio dei thread, il loro ID (in modo da poter distinguere l’allocazione di un nuovo thread dal pool rispetto ad un altro) ed il numero di worker thread disponibili nel pool.

Modulando i valori delle tre costanti NR_DATA (dimensione array da computare),NR_WORKER_THREADS (numero di elaborazioni parallele) e SCHEDULING_DELAY (delay tra una richiesta di elaborazione e la successiva) è possibile descrivere la variazione del comportamento del CLR nell’utilizzo del thread pool. E’ da precisare che i test, saranno eseguiti su una CPU dual core.

Partiamo con i seguenti valori ed osserviamone il risultato :

  • SCHEDULING_DELAY = 10;
  • NR_WORKER_THREADS = 2;
  • NR_DATA = 100000000;

7077.worker_thread_1_7CA54CFE

SCHEDULING_DELAY = 10; NR_WORKER_THREADS = 2; NR_DATA = 100000000;

Si osserva, sulla base delle due elaborazioni parallele richieste, che vengono allocati due thread separati nel pool (i due ID sono differenti ed il numero di worker thread disponibili è decrementato di due rispetto la condizione iniziale) e che vengono eseguiti parallelamente dai due core della CPU (il tempo di esecuzione è pressoché lo stesso).

Riduciamo drasticamente il tempo necessario all’elaborazione portando il valore NR_DATA a 1000.

1004.worker_thread_2_618C7DF0

SCHEDULING_DELAY = 10; NR_WORKER_THREADS = 2; NR_DATA = 1000;

In questo caso, osserviamo che l’elaborazione è così rapida che il CLR tende ad eseguire le due richieste in maniera sequenziale riutilizzando il medesimo thread (stesso ID). In questo caso, sarà stato utilizzato un solo core della CPU. Ovviamente, una situazione simile si ottiene aumentando il delay tra un avvio e l’altro considerando un tempo di elaborazione lungo, in quanto si dà al CLR il tempo di riciclare lo stesso thread nel pool.

4555.worker_thread_3_47B847C1

SCHEDULING_DELAY = 1000; NR_WORKER_THREADS = 2; NR_DATA = 100000000;

Proviamo ad aumentare il numero di elaborazioni parallele portandole a 4. E’ ovvio che, avendo a disposizione una CPU dual core, sarà possibile parallelizzare realmente solo 2 computazioni ed eseguire più context switch per fornire il multithreading per le altre elaborazioni. Ovviamente il CLR tenderà ad utilizzare più pool nel thread, se assumiamo un tempo di elaborazione piuttosto lungo. Un possibile output sarà il seguente.

2526.worker_thread_4_78D78261

SCHEDULING_DELAY = 10; NR_WORKER_THREADS = 4; NR_DATA = 100000000;

In questo caso, due elaborazioni vengono avviate quasi contemporaneamente e per la terza viene allocato un nuovo thread. Al termine della prima elaborazione, viene avviata la quarta riciclando il thread di quella appena terminata (ID = 11).

CLR – Thread Pool : Introduzione

Generalmente, la creazione di un thread è molto costosa in termini di risorse da allocare ed in termini di tempo. Inoltre, avere molti thread attivi nel sistema per poter eseguire molteplici operazioni in parallelo tende a ridurre le performance del sistema stesso, in quando il processore è costretto ad eseguire numerosi context switch durante lo scheduling.

Per ottimizzare l’utilizzo delle risorse e migliorare le performance, ci viene in aiuto il thread pool fornito dal CLR. Bisogna precisare, però, che la funzionalità del thread pool è nativa in Windows e che tutte le classi ed i metodi forniti dal CLR ne rappresentano comunque un wrapper.

1727.image_5B05635A

Attraverso il thread pool, il CLR fornisce appunto un pool di thread che possiamo utilizzare per eseguire elaborazioni in parallelo. Il manager del thread pool ha a disposizione una coda all’interno della quale pervengono tutte le richieste di elaborazione parallela fatte dalla nostra applicazione. Ogni qual volta viene inserita una nuova richiesta, il manager analizza se nel thread pool è disponibile un thread libero in stato di wait e pronto per poter essere assegnato all’esecuzione di una nuova elaborazione. Nel caso affermativo, viene eseguita tale assegnazione e la computazione da noi richiesta viene eseguita all’interno di un thread che deve essere solo avviato e non creato (in quanto già esistente) con un notevole risparmio di tempo (soprattutto in fase di avvio) e di risorse (non dobbiamo allocare il contesto di un nuovo thread). Viceversa, nel caso in cui, un thread non fosse disponibile nel pool, esso viene creato (ovviamente con una riduzione di performance e maggior utilizzo di risorse) ed assegnato all’elaborazione da eseguire. Nel momento in cui, però, un thread del pool termina la sua elaborazione esso non viene eliminato ma rimesso nel pool in stato di wait e pronto per essere assegnato ad altre richieste. L’eliminazione del thread, per risparmiare risorse allocate, viene eseguita solo nel caso in cui un thread non viene utilizzato per un certo tempo definito nel CLR.

Da questo scenario si evince che i vantaggi possono essere :

  • riduzione al minimo dell’allocazione di risorse;
  • velocità nell’avvio di un’elaborazione parallela;
  • miglioramento generale delle performance;

I parametri che possono, però, influenzare l’utilizzo ottimale del pool sono molteplici ed in particolare :

  • frequenza di schedulazione : intesa come la frequenza con cui la nostra applicazione richiede l’avvio di una computazione utilizzando il thread pool. E’ ovvio che se tale frequenza è elevata, ci saranno maggiori possibilità che il manager del pool debba allocare più thread dal pool stesso considerando che la coda delle richieste tende a popolarsi rapidamente. Con una frequenza di schedulazione bassa, probabilmente avremo più possibilità di riutilizzare lo stesso thread per eseguire tutte le richieste pervenute nella coda. E’ anche vero che ciò è legato alla durata della computazione (vedi dopo);
  • durata della computazione : indica la durata dell’elaborazione che vogliamo far eseguire ad un thread del pool. Più essa è alta e più un thread rimane occupato  e quindi il manager del pool dovrà allocare e utilizzare altri thread per esaudire ulteriori richieste nella coda;
  • numero di elaborazioni parallele : inteso come il numero di richieste che la nostra applicazione fa al manager del pool per eseguire elaborazioni parallele. Maggiore è il numero di richieste e maggiore è la probabilità che verranno utilizzati più thread nel pool per esaudirle;
  • numero di core del processore : questo aspetto hardware influisce soprattutto sulle performance, considerando che pur nel caso di più thread allocati dal pool, essi saranno distribuiti su più core e quindi la velocità di esecuzione sarà notevole;

Dalle considerazioni suddette, si evince che, per ottimizzare l’utilizzo di risorse del pool è necessario eseguire delle elaborazioni molto brevi, in modo che il manager sia in grado di riciclare sempre lo stesso thread per eseguirle. E’ anche vero che in questo caso le nostre operazioni verranno “sequenzializzate”. Un miglioramento delle performance a discapito di maggiori risorse allocate, lo otteniamo soprattutto nel caso in cui le computazioni sono più lunghe, per cui il manager è costretto ad allocare più thread, e magari usufruire di un processore multicore in modo da parallelizzare “realmente” le esecuzioni.

E’ ovvio che in questi termini, possono essere fatti numerosi ragionamenti alterando in un modo o nell’altro i parametri suddetti che vanno ad influenzare l’utilizzo ottimale del pool e delle risorse di sistema.

E’ inoltre importante sottolineare che, le computazioni per le quali richiediamo un’elaborazione parallela non devono prevedere dei meccanismi di sincronizzazione o accesso a risorse condivise, altrimenti decade il miglioramento di performance dovuto al parallelismo, soprattutto in un sistema multicore.

Tornando all’implementazione del pool nel CLR, esso fornisce due tipologie di thread :

  • worker threads : utilizzati per operazioni di tipo CPU bound;
  • completion port threads : thread che vengono avviati tipicamente al termine di operazioni asincrone su file o socket. L’uso di tali thread si basa proprio sul meccanismo delle completion port fornito da Windows. Infatti, ogni qual volta viene avviata un’operazione asincrona, uno dei meccanismi forniti da Windows per segnalare il termine di tale operazione, è quello di inserire un pacchetto in una coda specifica opportunamente monitorata da un thread, che nel nostro caso sarà proprio un completion port thread nel quale sarà eseguita la callback prevista al termine dell’operazione asincrona;

Prima di terminare questa breve introduzione per poi “sporcarci” le mani negli articoli successivi, anticipo quelli che saranno i possibili campi di utilizzo del thread pool :

  • worker thread : possibilità di utilizzare un thread per eseguire un’operazione CPU bound;
  • wait event : utilizzare un thread del pool per monitorare lo stato di un evento e di far eseguire una nostra callback nel momento in cui l’evento risulta segnalato oppure un eventuale timeout è scattato;
  • timer : uso di un thread per eseguire operazioni periodiche basate appunto su un timer;
  • I/O asincrono : possibilità di lanciare operazioni asincrone su file o socket per poi demandare ad un thread del pool il compito di eseguire una callback al termine di tale operazione;

Inside “Extension Methods” … call vs callvirt

Capita sempre di dover utilizzare delle classi delle quali non abbiamo il codice sorgente (basti pensare a quelle del .Net Framework) ma sulle quali non possiamo eseguire determinate operazioni in assenza dei corrispondenti metodi. Non abbiamo quindi la possibilità di estenderle e dotarle del nuovo comportamento di cui abbiamo bisogno.

In questi casi, risolviamo il problema implementando tale metodo al di fuori della classe stessa e ad esempio aggiungendolo in una nuova classe statica.

Per esempio, supponiamo di aver bisogno del metodo IndexOf() sulla classe StringBuilder (di cui essa non è dotata). Possiamo realizzare una nuova classe statica all’interno della quale implementiamo tale metodo.

public static class StringBuilderExtensions
{
    public static int IndexOf(StringBuilder sb, char value)
    {
        for (int index = 0; index < sb.Length; index++)
        {
            if (sb[index] == value)
                return index;
        }
        return -1;
    }
}

Ovviamente, per invocare tale metodo dobbiamo definire un’istanza della classe StringBuildere passare tale oggetto al metodo statico implementato.

int index;
StringBuilder sb = new StringBuilder("Paolo");
index = StringBuilderExtensions.IndexOf(sb, 'o');
Per fortuna, attraverso l’utilizzo degli Extension Methods, abbiamo la possibilità di estendere in maniera naturale una classe ed invocare direttamente sull’istanza dell’oggetto il nuovo comportamento implementato.
L’implementazione di un extension method viene realizzata con la tecnica precedente ma con la differenza nell’aggiungere la parola chiave this davanti al primo parametro del nuovo metodo, per indicare che esso sarà l’oggetto su cui andrà ad agire.
public static class StribgBuilderExtensionMethods
{
    public static int IndexOf(this StringBuilder sb, char value)
    {
        for (int index = 0; index < sb.Length; index++)
        {
            if (sb[index] == value)
                return index;
        }
        return -1;
    }
}
Attraverso questa tecnica, l’Intellisense di Visual Studio ci mette a disposizione tale nuovo metodo, direttamente sull’istanza della classe StringBuilder.
4520.em_1_62641764

A questo punto, possiamo però dimostrare che le due implementazioni sono perfettamente identiche e che la feature degli extension methods è più che altro una facility per lo sviluppatore.

Infatti, invocare un extension method equivale esattamente ad invocare un metodo statico come nel caso precedente con la differenza che non dobbiamo eseguire esplicitamente il passaggio del parametro su cui il metodo andrà ad agire. Basta confrontare le due seguenti invocazioni :

index = StringBuilderExtensions.IndexOf(sb, 'o');
index = sb.IndexOf('o');
Sarà il compilatore a generare il codice IL che passa il parametro “sb” all’extension method.
Consideriamo il seguente esempio con il relativo codice IL :
static void Main(string[] args)
{
    int index;
    StringBuilder sb = new StringBuilder("Paolo");
    sb.Append("Embedded Life");
    index = StringBuilderExtensions.IndexOf(sb, 'o');
    index = sb.IndexOf('o');
}
3482.em_2_5927DC23
Come si può osservare nel codice IL generato, le due invocazioni di IndexOf() sono perfettamente identiche ed il fatto che siano invocazioni di metodi statici lo evidenzia anche l’utilizzo della call al posto della callvirt come nel caso del metodo Append() che è un metodo di istanza della classe StringBuilder.

Infatti, esiste una sostanziale differenza tra call e callvirt :

  • call : può essere usata per l’invocazione di metodi statici, di istanza e virtuali. Tipicamente soprattutto per i metodi statici, preferendo la callvirt agli altri tipi di metodi. Essa assume che l’oggetto su cui viene eseguita la chiamata non sia null e quindi non esegue questo tipo di check;
  • callvirt : utilizzata per l’invocazione di metodi virtuali e di istanza, quindi non viene mai utilizzata per i metodi statici. Essa esegue sempre un check per verificare che l’oggetto su cui viene eseguita l’invocazione non sia null ed in tal caso viene sollevata una NullReferenceException;

La differenza sul check di oggetto null o meno lo si può apprezzare con il seguente esempio :

static void Main(string[] args)
{
    int index;
    StringBuilder sb = null;
    sb.Append("Paolo");
    index = sb.IndexOf('o');
}

Nel caso dell’invocazione del metodo Append() attraverso la callvirt, il CLR solleverà l’eccezione proprio in corrispondenza della chiamata.

7242.em_3_262825EF

Viceversa, nel caso dell’invocazione dell’extension method IndexOf() attraverso la call, l’invocazione viene eseguita (non esiste il check sul fatto che l’oggetto è null) ma ovviamente l’eccezione viene sollevata all’interno del metodo quando si tenta di accedere all’oggetto.

1586.em_4_63D50DAB

BeforeFieldInit … un type attribute invisibile ma determinante !

L’inizializzazione dei campi statici di una classe può essere eseguita con le due seguenti diverse modalità oppure con un mix di esse :

  • Type Initializer : inizializzazione di un campo statico in corrispondenza della sua dichiarazione;
  • Type Constructor : costruttore statico della classe che viene invocato una sola volta (per application domain) sulla stessa;

Ebbene, la presenza o meno dell’uno o dell’altro può produrre sequenze di esecuzioni differenti da parte del CLR sulla nostra classe.

Consideriamo le due seguenti classi, che a prima vista risultano semanticamente uguali :

class FirstClass
{
// type initializer
public static int x = 1;
}

class SecondClass
{
public static int x;

// type constructor
static SecondClass()
{
x = 1;
}
}

Entrambe hanno il campo statico “x”  ma che viene inizializzato in maniera diversa. Con unType Initializer nel primo caso e con un costruttore statico (Type Constructor) nel secondo. Il risultato finale sarà ovviamente lo stesso ma, se utilizziamo il Reflector oppure ILDASM, constatiamo come il compilatore abbia tradotto in codice IL le due classi in maniera leggerment diversa :

3056.beforefieldinit_1_57F8FD6D

0486.beforefieldinit_2_thumb_542B32D0

Alla classe che non ha il costruttore statico è stato aggiunto l’attributo beforefieldinit.
Cosa comporta la presenza di questo attributo ?
E’ noto che, nell’ambito di una stessa classe, l’invocazione di un Type Initializer avviene sempre prima del Type Constructor ed inoltre, l’inizializzazione di un campo statico di una classe viene eseguita ovviamente prima che si acceda a tale campo. Ma quanto prima ? Siamo certi che avviene immediatamente prima ? O magari viene eseguita molto tempo prima ? Se tentassimo di accedere al campo “x”  di ciascuna delle due classi, esso sarà stato inizializzato immediatamente prima dell’accesso o molto tempo prima ?
Per rendercene conto, consideriamo le due seguenti classi :
class BeforeFieldInit
{
    // type initializer
    public static string x = WriteLine("BeforeFieldInit Type Initializer");

    // static method
    public static string WriteLine(string s)
    {
        Console.WriteLine(s);
        return s;
    }
}

class NotBeforeFieldInit
{
    public static string x;

    // type constructor
    static NotBeforeFieldInit()
    {
        x = WriteLine("NotBeforeFieldInit Static Constructor");
    }

    // static method
    public static string WriteLine(string s)
    {
        Console.WriteLine(s);
        return s;
    }
}

 

Entrambe hanno un metodo statico per la scrittura di una stringa su console ma, mentre la prima sarà compilata con l’attributo beforefieldinit, la seconda non sarà dotata di questo attributo.
Considerando la seguente applicazione delle due classi suddette ed osserviamone l’output :

static void Main(string[] args)
{
    string tmp;
    Console.WriteLine("Starting Main");
    tmp = BeforeFieldInit.x;
    tmp = NotBeforeFieldInit.x;
    Console.ReadLine();
}
1033.beforefieldinit_3_41763919

Nonostante l’accesso al campo “x” della classe BeforeFieldInit sia l’istruzione 5, il Type Initializer viene invocato addirittura prima dell’istruzione 4. Mentre il costruttore statico della classe NotBeforeFieldInit viene invocato immediatamente prima dell’accesso al campo.

Ciò fa capire che la presenza dell’attributo beforefieldinit (aggiunto dal compilatore quando una classe non è dotato esplicitamente di costruttore statico), indica al CLR di poter invocare l’inizializzazione dei campi statici anche tempo prima che ci sia un accesso su di essi mentre l’assenza dell’attributo ritarda l’inizializzazione immediatamente prima all’accesso.

Conseguenze sulle performance

A questo punto, vediamo come questa semplice differenza possa comportare un impatto sulle performance di un’applicazione.

Consideriamo nuovamente le due classi FirstClass e SecondClass precedenti, dotate entrambe di un campo statico intero “x” ma la prima, ovviamente compilata con l’attributobeforefieldinit. Utilizziamole nel seguente applicativo di esempio :

static void PerformanceTest1()
{
    Stopwatch sw = Stopwatch.StartNew();
    for (int i = 0; i < 1000; i++)
    {
        FirstClass.x = 1;
    }
    Console.WriteLine("Performance access ... {0} : FirstClass", sw.Elapsed);

    sw = Stopwatch.StartNew();
    for (int i = 0; i < 1000; i++)
    {
        SecondClass.x = 1;
    }
    Console.WriteLine("Performance access ... {0} : SecondClass", sw.Elapsed);
}

static void Main(string[] args)
{
    PerformanceTest1();
    Console.ReadLine();
}

Vogliamo valutare il tempo necessario per eseguire 100 assegnazioni consecutive sul campo statico “x” di ciascuna classe. Il risultato è il seguente (i tempi possono ovviamente variare da computer a computer e tra un avvio e l’altro, ma il rapporto rimarrà comunque lo stesso) :

1033.beforefieldinit_3_41763919

Utilizzando la classe compilata con l’attributo beforefieldinit, abbiamo un miglioramento di performance dell’80% circa (5 volte più veloce) !!

A cosa è dovuta questa notevole differenza ?

Quando il JIT compiler compila il metodo PerformanceTest1() produce un codice macchina leggermente diverso :

  • nel caso di utilizzo della FirstClass, il Type Initializer viene invocato prima dell’ingresso del ciclo for;
  • nel caso di utilizzo della SecondClass, la chiamata al Type Constructor viene inserita all’interno del ciclo for ma ovviamente il costruttore statico dovrà essere invocato una sola volta (alla prima iterazione). Per questo motivo, il JIT compiler produce un codice che esegue un check per verificare se tale costruttore sia stato o meno invocato; ciò determina un peggioramento delle performance !

Per rendercene conto, riporto di seguito il codice macchina (mixato con le istruzioni C#) prodotto dal JIT compiler :

7563.beforefieldinit_5_7DDE87F6

Nel caso della FirstClass, constatiamo la presenza della sola istruzione che assegna il valore 1 al campo statico “x” della classe stessa.

2313.beforefieldinit_6_423E7936

Nel caso della SecondClass, prima dell’istruzione di assegnazione del valore 1 al campo statico “x”, notiamo la presenza di una call. Con questa call, viene invocato il costruttore statico e viene eseguito il check che verifica, quindi ad ogni iterazione, se sulla classeSecondClass il costruttore stesso sia stato già invocato in precedenza.

Aggiungiamo un’altra funzione al nostro esempio che verrà invocata nel main :

static void PerformanceTest2()
{
    Stopwatch sw = Stopwatch.StartNew();
    for (int i = 0; i < 1000; i++)
    {
        FirstClass.x = 1;
    }
    Console.WriteLine("Performance access ... {0} : FirstClass", sw.Elapsed);

    sw = Stopwatch.StartNew();
    for (int i = 0; i < 1000; i++)
    {
        SecondClass.x = 1;
    }
    Console.WriteLine("Performance access ... {0} : SecondClass", sw.Elapsed);
}

static void Main(string[] args)
{
    PerformanceTest1();
    PerformanceTest2();
    Console.ReadLine();
}

Osserviamo che la funzione PerformanceTest2() è volutamente uguale allaPerformanceTest1() e viene invocata subito dopo di essa nel main. Non ho utilizzato la stessa funzione chiamandola due volte, perché voglio forzare il JIT compiler ad eseguire una nuova compilazione (nel caso della doppia chiamata alla PerformanceTest1() ciò non accadrebbe). L’output sarà il seguente :

0830.beforefieldinit_7_5125A850

Eseguendo la funzione PerformanceTest2(), nonostante sia comunque diversa dallaPerformanceTest1() (anche se il codice è lo stesso), si osserva che le performance sono praticamente le stesse. Cosa è successo ? Semplicemente questo…

Quando il JIT compiler esegue la compilazione della funzione PerformanceTest2(), sa già che il costruttore statico della SecondClass è stato invocato (nella funzione PerformanceTest1()) e non genera il codice macchina che invoca il costruttore ed il check di verifica ad ogni iterazione.

5826.beforefieldinit_8_55BBB30A

Per quanto riguarda la classe FirstClass non è cambiato ovviamente nulla.

2046.beforefieldinit_9_3E40FED9

Viceversa, per la SecondClass non esiste più la call al costruttore statico della classe.

CLR : Inside memory model (Parte 2)

Introduzione

Nell’articolo precedente, abbiamo iniziato ad approfondire cosa accade nel momento in cui il CLR alloca un oggetto sulla memoria Heap e come vengono distribuite  le informazioni contenute nell’oggetto stesso, sia in termini di dati propri dell’oggetto che di dati aggiunti dal CLR.

In questo articolo, vedremo nel dettaglio come avviene l’invocazione dei metodi sugli oggetti con e senza polimorfismo.

La Method Table

Ciascun Type Object allocato sull’Heap ed associato ad uno specifico tipo, contiene al suo interno la Method Table, ossia la tabella dei metodi esposti dal tipo stesso che viene utilizzata dal CLR per l’invocazione di questi ultimi sugli oggetti.

Utilizzando il debugger avanzato SOS di Visual Studio, è possibile visualizzare la Method Table di un tipo. Eseguiamo in primo luogo il dump dell’oggetto bClass in modo da poter ricavare l’indirizzo della Method Table all’interno del corrispondete Type ObjectBaseClass Type Obj.

!DumpObj 00c2c3b0
PDB symbol for clr.dll not loaded
Name:        ConsoleApplication1.BaseClass
MethodTable: 009b391c
EEClass:     009b14d8
Size:        16(0x10) bytes
File:        C:\Documents and Settings\Developer\My Documents\Visual Studio 2010\Projects\ConsoleApplication1\ConsoleApplication1\bin\Debug\ConsoleApplication1.exe
Fields:
MT    Field   Offset                 Type VT     Attr    Value Name
79b9f9ac  4000001        4        System.String  0 instance 00c2c3c0 instanceString
79ba2978  4000002        8         System.Int32  1 instance        1 instanceInt
79ba2978  4000003       24         System.Int32  1   static        3 staticInt

Dall’esecuzione del comando di DumpObj, si evince che la Method Table si trova all’indirizzo 0x009b391c. Su tale indirizzo, possiamo eseguire il comando DumpMT :

!DumpMT -MD 009b391c
EEClass:      009b14d8
Module:       009b2e9c
Name:         ConsoleApplication1.BaseClass
mdToken:      4a0ec1b602000002
File:         C:\Documents and Settings\Developer\My Documents\Visual Studio 2010\Projects\ConsoleApplication1\ConsoleApplication1\bin\Debug\ConsoleApplication1.exe
BaseSize:        0x10
ComponentSize:   0x0
Slots in VTable: 9
Number of IFaces in IFaceMap: 0
————————————–
MethodDesc Table
Entry MethodDesc      JIT Name
79aaa7e0   79884934   PreJIT System.Object.ToString()
79aae2e0   7988493c   PreJIT System.Object.Equals(System.Object)
79aae1f0   7988495c   PreJIT System.Object.GetHashCode()
79b31600   79884970   PreJIT System.Object.Finalize()
009bc048   009b38e8     NONE ConsoleApplication1.BaseClass.VirtualMethod()
009bc038   009b38cc      JIT ConsoleApplication1.BaseClass..ctor()
009bc058   009b3904      JIT ConsoleApplication1.BaseClass..cctor()
009bc040   009b38d8     NONE ConsoleApplication1.BaseClass.InstanceMethod()
009bc050   009b38f4     NONE ConsoleApplication1.BaseClass.StaticMethod()

In primo luogo, osserviamo che il numero di slot nella tabella sono 9 (quanti sono i metodi invocabili sul tipo BaseClass). I primi quattro metodi sono ereditati dalla classe Object, che come sappiamo è la classe base per un qualsiasi oggetto nel .Net Framework. Si evince che essi hanno l’entry point (colonna Entry) ad un indirizzo di memoria completamente diverso dagli altri, in quanto l’oggetto Object Type Object, viene istanziato dal CLR all’avvio in una propria zona della memoria Heap ed il codice viene subito compilato dal JIT compiler (da notare la voce PreJIT).

Subito dopo, ci sono i tre metodi della classe BaseClass, che non essendo stati ancora invocati, non sono stati compilati dal JIT compiler (valore NONE). Viceversa, i due costruttori.ctor e .cctor (di istanza e di tipo, rispettivamente) sono stati eseguiti al momento della creazione dell’oggetto bClass e quindi compilati dal JIT compiler.

Invocazione di un metodo di istanza non virtuale…

A questo punto, eseguiamo la seguente riga di codice :

bClass.InstanceMethod();
Il metodo in questione è un metodo di istanza non virtuale, per cui il JIT compiler deve semplicemente individuare il tipo con cui è stato dichiarato l’oggetto bClassBaseClass, ed accedere alla Method Table del BaseClass Type Object corrispondente. Individuato il metodo, lo compila e lo esegue.
3730.mem_alloc_4_4DBEDD07

Figura 1

Se rieseguiamo il dump della tabella dei metodi :

009bc048   009b38e8     NONE ConsoleApplication1.BaseClass.VirtualMethod()
009bc038   009b38cc      JIT ConsoleApplication1.BaseClass..ctor()
009bc058   009b3904      JIT ConsoleApplication1.BaseClass..cctor()
009bc040   009b38d8      JIT ConsoleApplication1.BaseClass.InstanceMethod()
009bc050   009b38f4     NONE ConsoleApplication1.BaseClass.StaticMethod()

Osserviamo che il metodo InstanceMethod() risulta essere stato compilato dal JIT compiler.

… di un metodo virtuale …

Passiamo adesso ad invocare il metodo virtuale :

bClass.VirtualMethod();
In questo caso, avendo a che fare con un metodo virtuale, non basta al JIT compiler sapere con quale tipo è stata dichiarata la variabile bClass, perchè utilizzando l’ereditarietà, potremmo anche far puntare la variabile bClass (dichiarata come BaseClass) ad una istanza della classe derivata DerivedClass. In questo caso, il tipo utilizzato per la dichiarazione della variabile non coinciderebbe con il tipo dell’effettivo oggetto puntato (come vedremo successivamente) e la tecnica utilizzata per l’invocazione di un metodo di istanza non virtuale non funzionerebbe.
In una situazione del genere, il JIT compiler deve accedere all’oggetto effettivamente puntato dalla variabile bClass, cioè nel nostro caso il BaseClass Obj, e deve analizzare il campo Type Object Pointer per poter ricavare l’effettivo Type Object. In questo caso, esso è ancora ilBaseClass Type Obj. A questo punto, il JIT accede alla Method Table, individua il metodo, lo compila e lo esegue.
8802.mem_alloc_5_35C7D014

Figura 2

… infine di un metodo statico

Infine, vediamo cosa accade per l’invocazione del metodo statico :

BaseClass.StaticMethod();

Questa situazione, è molto simile all’invocazione del metodo di istanza non virtuale, con la differenze che il JIT compiler conosce immediatamente la classe a cui appartiene il metodo (considerato il modo in cui viene eseguita l’invocazione) e quindi conosce subito il Type Object (in questo caso il BaseClass Type Obj) nel quale cercare il metodo nella tabella.

Allocazione di un oggetto della classe derivata …

Passiamo ora a vedere cosa accade utilizzando un riferimento del tipo BaseClass, per puntare ad un oggetto della classe derivata DerivedClass e quindi invocare su di essa il metodo virtuale sfruttando a tutti gli effetti il concetto di polimorfismo.

In primo luogo, un’istanza della classe DerivedClass deve essere allocata :

bClass = new DerivedClass();

Con l’istruzione precedente, il CLR deve allocare in memoria Heap un’istanza della classeDerivedClass e fare in modo che il suo Type Object Pointer punti, ovviamente, alDerivedClass Type Obj. L’indirizzo assegnato al nuovo oggetto allocato verrà restituito nella variabile bClass.

3858.mem_alloc_6_5B4127A8

Figura 3

Osservando la Figura 3, si evince che il nuovo oggetto DerivedClass Obj è stato allocato sull’Heap e che la variabile bClass punta ad esso. Non c’è più alcun riferimento all’oggettoBaseClass Obj che quindi sarà sottoposto alla pulizia da parte del Garbage Collector.

Analizziamo nel dettaglio l’area di memoria in cui è stato allocato il nuovo oggetto :

5353.mem_alloc_7_15C8FAF2

Figura 4

In riferimento alla Figura 4, di seguito c’è il dettaglio dei valori contenuti in memoria :

  • Offset –4 (val. 00000000) : è il Sync Block Index;
  • Offset 0 (val. 009b39a4) : è il Type Object Pointer;
  • Offset +4 (val. 00c2c3c0) : campo instanceString, ereditato dalla classe baseBaseClass. Da osservare che l’indirizzo è ovviamente il medesimo;
  • Offset +8 (val. 00000001) : campo instanceInt, ereditato dalla classe base BaseClass;

… ed invocazione di un metodo virtuale overridden

Per concludere, vediamo cosa accade durante l’invocazione del metodo virtuale di cui la classe derivata ne fa l’override :

bClass.VirtualMethod();

In questo caso, il JIT compiler parte dalla variabile bClass che punta all’oggetto DerivedClass Obj sulla memoria Heap. A partire da tale oggetto, analizza il suo Type Object Pointer che punterà al DerivedClass Type Obj. In questo modo, il JIT compiler arriva alla Method Tabledella DerivedClass, compila ed esegue il metodo virtuale giusto e non quello della classe base.

0702.mem_alloc_8_27C518B2

Figura 5

Con questa seconda parte, si conclude l’approfondimento relativo al CLR memory model mettendo in evidenza che nonostante le notevoli semplificazioni che il .Net Framework ci fornisce per sviluppare le nostre applicazione nel modo più rapido possibile, l’architettura sottostante del sistema non è assolutamente semplice.