Versione per la stampa       Invia     
Valuta il contenuto e lascia un commento
MSDN
MSDN Library
Articoli tecnici
 Ottimizzare applicazioni Windows Fo...

  Attiva vista per larghezza di banda ridotta
Ottimizzare applicazioni Windows Forms

Di Corrado Cavalli - Microsoft MVP

In questa pagina

Introduzione Introduzione
                Utilizzo di NGen  Utilizzo di NGen
Migliorare le prestazioni grafiche Migliorare le prestazioni grafiche
 Esecuzione Asincrona  Esecuzione Asincrona
Conclusioni Conclusioni
Link Link

Introduzione

A prima vista il problema di ottimizzare un’applicazione sembrerebbe non esistere considerando che abbiamo personal computer sempre più potenti e che costano sempre meno.

In realtà, indipendentemente dalla potenza di calcolo disponibile, si finisce con l’avere una propria applicazione che per l’utente ha delle prestazioni inaccettabili e, quando ciò accade, dobbiamo iniziare la fase di ottimizzazione che potremmo sicuramente evitare abituandoci a scrivere codice correttamente eliminando quindi quei piccoli tranelli che contribuiscono a peggiorare le prestazioni del nostro programma.

Ottimizzazione dello startup

Dobbiamo innanzitutto distinguere i due diversi tipi di avvio che possiamo avere quando lanciamo un’applicazione.

  • Cold Startup
    E’ il tipo di avvio che abbiamo la prima volta che eseguiamo un’applicazione, tipicamente dopo che abbiamo avviato il computer.

  • Warm Startup
    E’ la modalità di avvio che abbiamo quando ri-eseguiamo un’applicazione appena terminata.


Nel primo caso, essendo necessario caricare le varie assemblies di .NET Framework insieme a quelle che compongono la nostra applicazione non c’è molto da fare, se non quella di ottimizzare l’accesso al disco. Nel secondo invece abbiamo diverse aree d’intervento.
Partiamo con lo sfatare una leggenda metropolitana: Le assemblies referenziate non influiscono sul tempo di caricamento poiché non vengono automaticamente caricate durante la fase di avvio, quello che invece impatta sono i tipi che vengono definiti nel codice di avvio.
Prendiamo ad esempio il codice che segue:

C#

static void Main (string[] args)
{
 if (args.Length == 0) 
 {
  Console.WriteLine("Ready to load");
  Console.ReadLine();
 }
 else
  Test t = null;//(*)Test è definito in un assembly referenziata da questo progetto
 LoadLib();
}

[MethodImpl(MethodImplOptions.NoInlining)]
void LoadLib()
{
 ModulesLib.Test t= new ModulesLib.Test(); 
 int v = t.AddOne(4);
 Console.WriteLine(v);
 Console.ReadLine();
}

VB

Sub Main(ByVal args() As String)
  If (args.Length = 0) Then
    Console.WriteLine("Ready to load")
    Console.ReadLine()
  Else
    Dim t As Test = Nothing
'(*)Test è definito in un assembly Lib1 referenziata da questo progetto
  End If
  LoadLib()
End Sub

<MethodImpl(MethodImplOptions.NoInlining)> _
Private Sub LoadLib()
 Dim t As New ModulesLib.Test()
 Dim v As Integer = t.AddOne(4)
 Console.WriteLine(v)
 Console.ReadLine()
End Sub

il quale, senza farne alcun uso, all’interno del metodo Main utilizza il tipo Test definito in un assembly ModulesLib referenziata dal progetto. Grazie all’aiuto offerto dal tool Process Explorer, (vedi Links) eseguendo questo esempio noteremo come, mentre il programma è fermo sulla chiamata al metodo Console.ReadLine l’assembly ModulesLib è già stata caricata quando, in realtà, l’utilizzo effettivo avviene al momento dell’esecuzione del metodo LoadLib.
Questo caricamento prematuro è legato alla presenza del tipo Test alla riga (*), rimuovendola, l’assembly ed eventuali altre assemblies dipendenti da essa, saranno caricate al momento di effettivo utilizzo diminuendo il tempo di avvio.

Nel caso abbiate assemblies che utilizzano un nome sicuro (strong-name) assicuratevi di metterle nella GAC (Global Assembly Cache) in questo modo eviterete il processo di verifica dell’integrità dell’assembly stessa ogni volta che la libreria viene caricata in quanto questo processo verrà eseguito una sola volta al momento dell’inserimento della dll, da parte dell’amministratore, nella GAC.

Utilizzo di NGen

L’analisi dei performance counters relativi al JIT Compiler può indicare che l’eccessivo tempo di avvio è causato dal massiccio intervento del JIT (Just-In-Time) compiler nel compilare tutto il codice necessario alla partenza. dell’applicazione.
Attraverso l’utility NGen.exe presente nel Framework .NET non solo è possibile azzerare il tempo di compilazione ma è anche possibile condividere le assemblies tra applicazioni, cosa che, causa presenza del JIT compiler, normalmente non avviene. (figura 1)

*

Affinché un assembly compilata con NGen sia condivisa da più applicazioni è necessario che questa non venga ribasata ovvero che sia possibile il suo caricamento all’indirizzo indicato alla voce Dll Base Address nelle proprietà del progetto alla voce Build->Advanced (Figura2). L’operazione di rebasing, oltre che essere onerosa in termini di tempo, richiedendo la riscrittura delle code-pages dell’assemblies le rende automaticamente private vanificando la possibilità di condivisione tra processi.

*

Nel caso si voglia utilizzare NGen correttamente, è perciò importante modificare l’indirizzo cercando di evitare conflitti con altre assemblies già caricate nel sistema, tale valore perde significato se non fate uso di NGen.
Supponendo di volere usare NGen con un' applicazione Foo.exe che referenzia FooLib1.dll la quale, a sua volta, referenzia FooLib2.dll il comando da utilizzare è: ngen /install Foo.exe
Risultato di questo comando è la generazione nell’area fusion riservata al Framework .NET (solitamente C:\Windows\assembly\NativeImages_v2.0.50727_32 ) delle immagini native Foo.ni.exe, FooLib1.ni.dll e FooLib2.ni.dll.
A questo punto lanciando Foo.exe il Framework .NET, verificata la presenza delle relative immagini native, anziché far intervenire il JIT compiler (mscorjt.dll) caricherà ed eseguirà le immagini native migliorando il warm startup.
Affinché il Framework .NET possa utilizzare le immagini native è necessario che queste rispecchino il contenuto delle assemblies originali (le quali devono essere comunque presenti),se ciò non accade, l’immagine nativa verrà ignorata e il processo di compilazione JIT entrerà di nuovo in azione vanificando l’utilizzo di NGen.
Ipotizziamo ora di sostituire FooLib2.dll con una versione aggiornata, risultato di quest’operazione è che, all’esecuzione successiva di Foo.exe sia FooLib2.dll che FooLib1.dll (in quanto utilizzatore di FooLib2.dll) non verranno caricate come immagini native.

Per evitare questo è necessario eseguire il comando: ngen /update il quale sincronizza tutte le immagini native con le assemblies originali.
Il comando /update dovendo analizzare tutte le immagini può impiegare parecchio tempo, fortunatamente la versione 2.0 di NGen offre la possibilità di eseguire comandi batch i quali verranno processati autonomamente dal servizio Microsoft .NET Framework NGen installato sulla macchina dall’installazione del Framework 2.0 (figura 3) il quale è attivo esclusivamente in presenza di comandi NGen da processare.

*

L’esecuzione di un comando batch avviene utilizzando il comando /queue:X (es: NGen /update /queue:3) dove X indica la priorità di esecuzione da 1 a 3 dove 3 significa: esegui il comando quando la macchina è in attesa.

Oltre ai comandi batch la versione 2.0 di NGen contiene altre interessanti novità:

  • La capacità di compilare automaticamente tutte le dipendenze esplicite di un’applicazione.

  • La possibilità di condividere assemblies tra application domains.

  • Miglioramento generale delle prestazioni.


Come tutte le tecniche di ottimizzazione anche l’utilizzo di NGen deve essere sottoposto a misurazione in quanto, non è da escludere, che il suo impiego non possa portare i benefici sperati in quanto le immagini native sono normalmente più grandi (circa 3 volte) rispetto alle immagini di origine quindi l’eventuale guadagno dovuto all’azzeramento del tempo di compilazione potrebbe essere annullato dal maggior tempo di caricamento e/o eventuale rebasing.

Ottimizzare l’utilizzo dei controlli Windows
Alcuni controlli (esempio: Listbox, Treeview, ListView…) permettono di evitare l’inutile ridisegno del proprio contenuto quando vengono inseriti dei nuovi elementi mediante una coppia di metodiBeginUpdatee EndUpdate oppure utilizzando in alternativa al metodo Add il metodo AddRange.

Prendendo come esempio il codice indicato di seguito:

VB

listBox2.Items.Clear()
Dim sw As new Stopwatch()
‘sw.Start()
listBox2.BeginUpdate()
for i As Integer = 0 To 10000
 listBox2.Items.Add(i.ToString())
Next
listBox2.EndUpdate();
‘sw.Stop();
MessageBox.Show(sw.ElapsedMilliseconds.ToString())

C#

listBox2.Items.Clear();
Stopwatch sw = new Stopwatch();
//sw.Start();
listBox2.BeginUpdate();
for (int i = 0; i < 10000; i++)
{
 listBox2.Items.Add(i.ToString());
}
//listBox2.EndUpdate();
sw.Stop();
MessageBox.Show(sw.ElapsedMilliseconds.ToString());

L’ordine d’incremento delle prestazione è di circa 10:1 quando si utilizzano BeginUpdate ed EndUpdate.
I controlli ListView e Treeview hanno delle prestazioni diverse a seconda se popolati in presenza o assenza dell’handle associato da Windows, nello specifico la ListView è leggermente più veloce in presenza dell’ handle mentre il Treeview si comporta in maniera opposta.

Quando si utilizza databinding è importante sganciare tutti i controlli durante l’aggiornamento della fonte dati, questo per evitare inutili refresh dell’interfaccia utente. Nel caso si utilizzi l’oggetto BindingSource ciò può avvenire mediante i metodi SuspendBinding e ResumeBinding. Lo stesso BindingSource può evitare inutili aggiornamenti dei controlli associati (es: DataGridView) impostando la proprietà RaiseChangeListEvents a false durante la fase di modifica dei dati associati alla proprietà DataSource, anche in questo caso le prestazioni possono migliorare in rapporto 10:1.
Uno degli errori più comuni legati al databinding è associare una fonte dati in questo modo:

listBox1.DataSource = table;
listBox1.DisplayMember="CustomerName";
listBox1.ValueMember="CustomerID";

Il problema di questo esempio è che, la proprietà ValueMember se valorizzata in presenza di dati provoca una successiva ri-associazione degli stessi con conseguente ripopolamento, cosa che è possibile evitare semplicemente scrivendo:

listBox1.DisplayMember="CustomerName";
listBox1.ValueMember="CustomerID";
listBox1.DataSource = table;

Quando l’interfaccia utente è generata aggiungendo controlli a runtime è importante considerare l’impatto che l’evento di layout scatenato ogni volta che andiamo a modificare direttamente o indirettamente alcune proprietà dei controlli contenitori (GroupBox, Panels, Forms…) può avere sulla durata dell’intera operazione.
Normalmente questo evento si occupa di posizionare automaticamente i controlli secondo la relativa proprietà Anchoring e/o Docking e, se il numero di controlli è elevato può impattare sulle prestazioni generali. L’utilizzo dei metodi SuspendLayout e ResumeLayout permette di eseguire l’operazione di riposizionamento dei controlli una sola volta.

Migliorare le prestazioni grafiche

La modalità di aggiornamento di un’area grafica da parte di GDI è composta da due fasi: pulizia e aggiornamento dello sfondo e ridisegno della parte in primo piano. Tecnicamente il tutto si traduce nell’invio da parte di Windows di due messaggi WM_ERASEBKG e WM_PAINT che il Framework .NET mappa rispettivamente sui metodi OnPaintBackground e OnPaint. Quando queste due fasi sono particolarmente onerose (ad esempio a causa della presenza di un’immagine di sfondo) l’operazione di aggiornamento comporta un noioso effetto “flickering”. Per evitare questo è possibile far si che le operazioni di pulizia e rinfresco dell’area grafica avvengano in un buffer in memoria che verrà successivamente copiato all’interno del controllo con un risultato decisamente migliore. Questa alternativa si abilita impostando la proprietà DoubleBuffered del controllo a True (figura 4)

*


A causa della creazione del buffer grafico quest’opzione può aumentare il consumo di memoria e quindi va attivata solo quando effettivamente necessario.
Nel caso si desideri richiedere l’aggiornamento di una parte grafica in alternativa ai metodi Update e Refresh è consigliabile utilizzare il metodo Invalidate il quale permette di indicare la regione che è stata invalidata consentendo così l’aggiornamento di una singola area anziché agire sull’intero contenuto.

Esecuzione Asincrona

Quando il collo di bottiglia è identificato nell’esecuzione di un metodo ben preciso, l’unica soluzione consiste nell’eseguire lo stesso in un thread separato consentendo così al thread principale, che solitamente gestisce l’interfaccia utente, di essere comunque reattivo. Essendo una necessità abbastanza frequente .NET Framework mette a disposizione tutto quello che serve per eseguire metodi in maniera asincrona in modo facile utilizzando seguendo lo stesso pattern utilizzato da parecchie classi presenti nella BCL (Base Class Library)

Immaginiamo di avere il seguente metodo DoLongTask, il quale impiegando parecchio tempo nell’esecuzione si presenta come ottimo candidato per un’esecuzione asincrona.

VB

public Function DoLongTask () as Integer
{
 Dim i As integer= 0
 While (i < 300000000)
  i+=1 
 End While
 return i;
}

C#

public int DoLongTask ()
{
 int i = 0;
 while (i++ < 300000000) { }
 return i;
}

Grazie alla classe Delegate, la quale implementa internamente il meccanismo di chiamata asincrona, possiamo scrivere:


VB

Private Delegate Function TaskDelegate()
As Integer ‘Notare come la firma del delegate coincida con DoLongTask

Dim del As TaskDelegate = new TaskDelegate(DoLongTask)
Dim ret As IAsyncResult = del.BeginInvoke(Nothing, Nothing)
Dim value As Integer = del.EndInvoke(ret)
listBox1.Items.Add(value)

C#

private delegate int TaskDelegate (); //Notare come la firma del delegate coincida con DoLongTask

TaskDelegate del = new TaskDelegate(DoLongTask);
IAsyncResult ret = del.BeginInvoke(null, null);
int value = del.EndInvoke(ret);
listBox1.Items.Add(value);

In questo caso, l’invocazione di BeginInvoke causa l’esecuzione di DoLongTask in un thread separato preso dal pool di thread gestiti da Framework .NET. Il problema vero è che eseguendo il codice sopra riportato avremo comunque il thread principale occupato in quanto la chiamata a EndInvoke è sincrona in attesa che il metodo DoLonkTask termini. Per evitare questo abbiamo due possibilità, la prima, riportata di seguito, consiste nell’attendere il completamento del metodo DoLongTask prima di invocare EndInvoke.

VB

Dim del As TaskDelegate = new TaskDelegate(DoLongTask)
Dim ret As IAsyncResult = del.BeginInvoke(Nothing, Nothing)
While (Not ret.IsCompleted)
 Application.DoEvents()
End While
Dim value As Integer = del.EndInvoke(ret)
listBox1.Items.Add(value)

C#

TaskDelegate del = new TaskDelegate(DoLongTask);
IAsyncResult ret = del.BeginInvoke(null, null);
while (!ret.IsCompleted)
{
 Application.DoEvents();
}
int value = del.EndInvoke(ret);
listBox1.Items.Add(value);

Il secondo, sicuramente più conveniente, consiste nel passare al metodo BeginInvoke un delegate il quale verrà richiamato non appena l’operazione DoLongTask sarà completata.

VB#

Dim context As SynchronizationContext
...
Context= WindowsFormsSynchronizationContext.Current
Dim del As TaskDelegate = new TaskDelegate(DoLongTask)
Dim ret As IAsyncResult = del.BeginInvoke(LongTaskCompleted, del)
...

Private Sub LongTaskCompleted (result As IAsyncResult)
  Dim del As TaskDelegate = DirectCast(result.AsyncState ,TaskDelegate)
  Dim value As Integer = del.EndInvoke(result)
  context.Send(New SendOrPostCallback(AddressOf callback), Nothing)
End Sub

Private Sub callback(ByVal o As Object)
  listBox1.Items.Add(value);
End Sub

C#

SynchronizationContext context;
...
context = WindowsFormsSynchronizationContext.Current;
TaskDelegate del = new TaskDelegate(DoLongTask);
IAsyncResult ret = del.BeginInvoke(LongTaskCompleted, del);
...

private void LongTaskCompleted (IAsyncResult result)
{
  TaskDelegate del = (TaskDelegate)(result.AsyncState);
  int value = del.EndInvoke(result);
  context.Send(delegate(object o)
  {
    listBox1.Items.Add(value);
  }, null);
}

L’esempio sopra indicato passa a BeginInvoke la funzione (LongtaskCompleted )da richiamare al termine dell’operazione e un parametro (del) che verrà reso disponibile come proprietà AsyncState del parametro result passato alla funzione di callback. Il codice contenuto in LongTaskCompleted, eseguito al termine dell’esecuzione asincrona di DoLongTask, recupera da AsyncState il delegate sul quale richiamare EndInvoke (operazione necessaria anche in assenza di valori di ritorno) dopodiché attraverso l’aiuto della classe WindowsFormsSyncronizationContext presente nel Framework 2.0 aggiunge, in maniera thread-safe, il valore ottenuto a una listbox.

La necessità d’impiego di WindowsFormsSyncronizationContext è reso necessario dal fatto che l’esecuzione del metodo LongtaskCompleted avviene nello stesso thread utilizzato per eseguire in maniera asincrona il metodo DoLongTask.

Conclusioni

In quest’articolo abbiamo visto alcune indicazioni su com’è possibile ottimizzare alcune parti critiche delle applicazioni Windows. Quando si parla di ottimizzazioni bisogna sempre valutare attentamente il contesto in cui ci si trova ed è molto importante misurare l’effettivo beneficio di una tecnica rispetto a un'altra.
L’imparare a evitare gli errori più comuni con il supporto di strumenti adatti (Profilers) che aiutano a capire quali parti del nostro codice richiedono più attenzione possono rendere questa fase dello sviluppo decisamente meno onerosa.

Link

(informazioni in lingua Inglese)

Process Explorer

Elenco comandi NGen

Improving Managed Code Performances

Performance Tips and Tricks in .NET Applications


© 2009 Microsoft Corporation. Tutti i diritti riservati. Condizioni per l'utilizzo  |  Marchi  |  Informativa sulla privacy
Page view tracker