(...continua da http://www.marcorossignoli.it/post/se-hai-il-pc-lento-e-perche-hai-poca-ram(Parte-1).aspx)
…quando un programma vuole allocare della memoria per poi utilizzarla deve prima o poi richiederla al sistema operativo attraverso delle API come ad esempio VirtualAlloc.
Quando parlo di programma, non intendo solamente il codice che noi scriviamo, ma anche ad esempio un qualsiasi runtime che effettua questa chiamata per noi.
Ad esempio il runtime .NET effettua questa operazione quando noi facciamo le new() sugli oggetti, ma fortunatamente tutta questa complessità ci è nascosta e la gestione della memoria viene fatta in modo trasparente dal garbage collector.
Per capire un pò meglio come funziona il tutto, scriviamo un pò di codice C#, precisamente una bella console application che farà in modo esplicito una richiesta di memoria virtuale al sistema operativo.
Intanto con un pò di P/Invoke permettiamo a CLR di chiamare l’API (vi consiglio questo tool per generare le firme in .NET Invoke Interop Assistant):
static class Native
{
public const int MEM_RESERVE = 8192;
public const int PAGE_READWRITE = 4;
public const int MEM_COMMIT = 4096;
[System.Runtime.InteropServices.DllImportAttribute
("kernel32.dll",EntryPoint ="VirtualAlloc",SetLastError=true)]
public static extern System.IntPtr VirtualAlloc(
[System.Runtime.InteropServices.InAttribute()] System.IntPtr lpAddress,
uint dwSize,
uint flAllocationType,
uint flProtect);
}
Attraverso VirtualAlloc possiamo così richiedere memoria (virtuale, ricordiamoci che noi possiamo usare solo quella) al sistema operativo che poi utilizzeremo per fare qualcosa, come ad esempio metterci le nostre strutture dati. E adesso il main:
static void Main(string[] args)
{
uint Megabyte = 1048576;
IntPtr memoryPointer = IntPtr.Zero;
memoryPointer = Native.VirtualAlloc(memoryPointer,
Megabyte * 300,
Native.MEM_RESERVE,
Native.PAGE_READWRITE);
if (memoryPointer == IntPtr.Zero )
throw new Win32Exception();
Console.ReadKey();
}
Il codice richiede semplicemente 300 megabyte di memoria virtuale sulla quale il processo possa scrivere e leggere.
In caso l’API ritorni null (IntPtr.Zero) significa che non c’è sufficiente memoria e l’allocazione non è andata a buon fine (ad esempio se noi mettiamo 3000 invece che 300 l’API ci risponde picche in quanto abbiamo visto che lo spazio di indirizzamento di default per lo user mode è di 2GB quindi non è possibile fare un’allocazione superiore a quella).
Se guardiamo bene il codice ci accorgiamo che l’API ci richiede un parametro particolare nel quale io ho passato MEM_RESERVE, questo significa che il sistema operativo non mi mette a disposizione subito 300 megabyte di ram fisica, ma semplicemente richiedo che mi vengano riservati 300 megabyte dello spazio di indirizzamento che succesivamente renderò fisici richiedendolo attraverso la stessa api, ma passando il parametro MEM_COMMIT. Riservarsi memoria virtuale non significa che posso metterci qualcosa, ma semplicemente mi permette di creare un blocco di memoria e avere la sicurezza che quando ne farò il commit (quindi verrà fisicamente messa a disposizione) avrò un insieme di locazioni contigue in memoria, nelle quali potrò mettere quello che mi serve.
Detto questo si capisce che abbiamo 2 tipi memoria nel nostro processo, la memoria virtuale e la memoria fisica effettivamente utilizzabile, tecnicamente chiamata “committed memory” alla quale mi riferirò sempre attraverso indirizzi virtuali. Quindi la memoria effettiva consumata dal mio processo non è quella virtuale ma quella committed ovvero quella che è stata effettivamente mappata ad indirizzi fisici.
Per provare questo comportamento possiamo fare un test e verificare che riservare memoria virtuale non consuma effetivamente la memoria fisica del sistema. Per effettuare questo test dovete scaricarvi un tool Process Explorer di Mark Russinovich, un architetto del core di Windows che lavora nel team di microsoft e co-autore del libro che vi ho segnalato come fonte.
A questo punto eseguiamo il codice che segue e guardiamo un pò di numeri:
1 static void Main(string[] args)
2 {
3 uint Megabyte = 1048576;
4 IntPtr memoryPointer = IntPtr.Zero;
5 memoryPointer = Native.VirtualAlloc(memoryPointer,
Megabyte * 300,
Native.MEM_RESERVE,
Native.PAGE_READWRITE);
6 if (memoryPointer == IntPtr.Zero )
throw new Win32Exception();
7 memoryPointer = Native.VirtualAlloc(memoryPointer,
Megabyte * 300,
Native.MEM_COMMIT,
Native.PAGE_READWRITE);
8 Console.ReadKey();
9 }
Tanto per cominciare vediamo a quanto ammonta la committed memory nel mio sistema nel momento in cui scrivo. Per vederla ho cliccato sull’icona
nella barra sotto il menù classico.
Ecco i risultati:
Circa 2.9 di memoria fisica allocata. Nel riquadro Commit Charge vediamo le stime meno approssimative.
Ora facciamo partire il nostro programma…mettiamo un bel breakpoint alla riga numero 6 e vediamo lo stato della memoria:
come possiamo notare è cambiato poco niente…un pò di committed in più ma poca roba (qualcosa ha dovuto caricare, alla fine un processo in più in memoria c’è e fino al breakpoint ci siamo arrivati, da qualche parte quei byte li dobbiamo mettere). Quindi possiamo affermare che allocare memoria virtuale non consuma memoria fisica effettiva.
Ora mettiamo un’altro breakpoint alla riga 8 e vediamo:
come possiamo vedere ora stiamo consumando circa 300 megabyte di memoria fisica (committed memory) in più (non sono precisamente 300 ma un pò di più…ma sul mio pc girano molti processi e quello è un counter globale, chissà quale altro processo ha richiesto qualcosa :-) ). Quindi possiamo affermare una volta per tutte che la memoria virtuale non è la memoria realmente utilizzata.
Vediamo ora il consumo di memoria dalla prospettiva del nostro processo. Per fare questo dovete impostare un paio di contatori utili. Andate sul menù View->Select Columns->Process Memory e fleggate Private Bytes e Virtual Size.
Il contatore Private Bytes tiene conto della committed memory consumata dal processo, mentre Virtual Size è semplicemente la memoria virtuale riservata per quel processo. Attenzione quello che ho scritto non è propriamente vero o meglio è impreciso, un processo può consumare memoria fisica privatamente (Private Bytes), ma potrebbe anche consumare memoria fisica attraverso l’utilizzo delle “page file backed section” che sono delle zone di committed memory condivise con altri processi(vengono chiamate così in quanto possono essere scritte sul file di paging se serve, vedi section objects) che non vengono conteggiate nel counter di un processo.
Passiamo ai numeri:
le colonne sono rispettivamente PID (id del processo) Virtual Size e Private Byte
Riga 6
Riga 8
Ancora una volta possiamo notare la differenza tra memoria virtuale e committed memory (memoria fisica allocata).
Quindi diciamo che la somma di tutta la committed memory di tutti i processi che girano nel nostro sistema ci da più o meno la stima di quanta RAM ci serve…(anche quì ho omesso le “page file backed section” e qualche altro dettaglio per semplicità).
Un ultimo cenno aspetta al paging file; in caso la memoria fisica fosse troppo poca per far girare i nostri programmi, windows può avvalersi del disco fisso come storage temporaneo, permettendoci così di aumentare il “commit limit” del nostro pc. Chiaramente il codice e i dati di un programma devono essere in RAM per essere utilizzati quindi sarà compito del sistema operativo usare policy di “caricamento” e “scaricamento” sul file di paging delle pagine di memoria meno utili. Perciò aumentare il file di pagin significa semplicemente avere più committed memory disponibile, certo se per esempio (esagerato ma è per rendere l’idea) abbiamo 2GB di ram e abbiamo bisogno di 6GB di page file per non andare in out of memory significa che la nostra attività consuma mediamente 4 volte la ram fisica che ho (2GB fisica + 6GB file di paging) e in questo modo l’attività di paging (scrivere sul file di paging e leggere da esso) potrebbe peggiorare seriamente le prestazioni della macchina. Quindi non esiste una formula assoluta per dimensionare il file di pagin, ma possiamo controllare il nostro utilizzo medio per esempio guardando per un periodo alla sera prima di andare a casa il contatore Peak (riquadro Commit Charge) con Process Explorer (vedi sopra) e fare la differenza con il contatore Total (riquadro Physical Memory) così da vedere di quanta memoria in più abbiamo mediamente bisogno, impostando il file di paging intorno a quel numero.
Spero con questi 2 blog di aver chiarito meglio il motivo per cui, se il pc è lento, non è sempre colpa della RAM, ma per saperlo con certezza servono come sempre i numeri…l’idea di questo post mi è venuta quando l’azienda che mi fa l’hosting di un server fisico mi ha consigliato di aumentare la ram se volevo passare da Sql Server 2005 a Sql Server 2008, ho risposto che per ora i numeri non mi dicono che serve un upgrade di RAM ;-)
…chissà se l’indovino aveva ragione…
Fonti: http://www.microsoft.com/learning/en/us/Books/6710.aspx