2013-03-31 17 views
10

Sto lavorando a una simulazione di fluido in C#. Ogni ciclo ho bisogno di calcolare la velocità del fluido in punti discreti nello spazio. Come parte di questo calcolo, ho bisogno di alcune decine di kilobyte per lo spazio di scrittura per contenere alcuni array double [] (la dimensione esatta degli array dipende da alcuni dati di input). Gli array sono necessari solo per la durata del metodo che li usa, e ci sono alcuni metodi diversi che richiedono uno spazio come questo.Scratch di memoria in ambiente gestito

come la vedo io, ci sono un paio di soluzioni diverse per costruire le matrici zero:

  1. Usa 'nuovo' per afferrare la memoria dal mucchio ogni volta che il metodo viene chiamato. Questo è quello che stavo facendo all'inizio, tuttavia mette molta pressione sul garbage collector, ei picchi di pochi ms una o due volte al secondo sono davvero fastidiosi.

  2. Gli array scratch sono passati come parametri durante la chiamata dei metodi. Il problema è che questo costringe l'utente a gestirli, incluso il dimensionamento appropriato, il che è un enorme problema. Inoltre, rende difficile l'utilizzo di più o meno memoria scratch, poiché modifica l'API.

  3. Utilizzare stackalloc in un contesto non sicuro per allocare la memoria scratch dallo stack di programmi. Funzionerebbe bene, tranne che dovrei compilare con/non sicuro e cospargere costantemente blocchi non sicuri nel mio codice, cosa che vorrei evitare.

  4. Preallocare gli array privati ​​una volta all'avvio del programma. Questo va bene, tranne che non conosco necessariamente la dimensione degli array di cui ho bisogno fino a quando non riesco a vedere alcuni dei dati di input. E diventa davvero complicato dal momento che non è possibile limitare l'ambito di queste variabili private a un solo metodo, in modo da inquinare costantemente lo spazio dei nomi. Ed è scarsamente scalabile in quanto il numero di metodi che necessitano di una memoria di memoria aumenta, dal momento che sto allocando molta memoria che viene utilizzata solo una frazione del tempo.

  5. Creare una sorta di pool centrale e allocare gli array di memoria scratch dal pool. Il problema principale con questo è che non vedo un modo semplice per allocare array di dimensioni dinamiche da un pool centrale. Potrei usare un offset iniziale e una lunghezza e disporre che tutta la memoria scratch condivida essenzialmente un unico grande array, ma ho un sacco di codice esistente che assume il doppio [] s. E dovrei fare attenzione a rendere sicuro un thread di questo tipo.

...

Qualcuno ha qualche esperienza con un problema simile? Qualche consiglio/lezione da offrire dall'esperienza?

+0

Intendevi davvero qualche decina di kilobyte? Dato che è una quantità così piccola, non mi preoccuperei della gestione della memoria per questo ... –

+0

Non sembra molto, ma se eseguo 2000 cicli/sec, all'improvviso è qualcosa come 60 MB/sec, e il GC inizia a notare. –

+0

@JayLemmon, penso che ti preoccupi di questi dettagli per motivi di prestazioni, giusto? Se il tuo progetto non è finito, ti suggerisco di non preoccuparti delle prestazioni fino alla sua conclusione. Vedi questo articolo su [Ottimizzazione prematura] (http://c2.com/cgi/wiki?PrematureOptimization). Se il progetto è finito, l'articolo fornisce interessanti osservazioni anche su __ottimizzazione__ in generale. Cito una parte: "Un malinteso comune è che il codice ottimizzato è necessariamente più complicato [...] codice fattorizzato spesso corre più veloce e utilizza anche meno memoria [...]". – jay

risposta

3

Puoi avvolgere il codice che utilizza tesi graffiare gli array in un utilizzando dichiarazione come questa:

using(double[] scratchArray = new double[buffer]) 
{ 
    // Code here... 
} 

Ciò consentirà di liberare la memoria in modo esplicito chiamando il descructor alla fine l'istruzione using.

Purtroppo sembra che quanto sopra non sia vero! Al posto di questo, puoi provare qualcosa sulla falsariga di avere una funzione di aiuto che restituisce una matrice della dimensione appropriata (potenza più vicina di 2 maggiore della dimensione), e se non esiste, creala. In questo modo, avresti solo un numero logaritmico di array. Se si voleva che fosse thread-safe, sarebbe necessario andare un po 'più nei guai.

Potrebbe essere simile a questa: (usando pow2roundup da Algorithm for finding the smallest power of two that's greater or equal to a given value)

private static Dictionary<int,double[]> scratchArrays = new Dictionary<int,double[]>(); 
/// Round up to next higher power of 2 (return x if it's already a power of 2). 
public static int Pow2RoundUp (int x) 
{ 
    if (x < 0) 
     return 0; 
    --x; 
    x |= x >> 1; 
    x |= x >> 2; 
    x |= x >> 4; 
    x |= x >> 8; 
    x |= x >> 16; 
    return x+1; 
} 
private static double[] GetScratchArray(int size) 
{ 
    int pow2 = Pow2RoundUp(size); 
    if (!scratchArrays.ContainsKey(pow2)) 
    { 
     scratchArrays.Add(pow2, new double[pow2]); 
    } 
    return scratchArrays[pow2]; 
} 

Edit: thread-safe versione: Questo avrà ancora cose che vengono garbage collection, ma sta andando ad essere specifico thread e dovrebbe essere molto meno overhead.

[ThreadStatic] 
private static Dictionary<int,double[]> _scratchArrays; 

private static Dictionary<int,double[]> scratchArrays 
{ 
    get 
    { 
     if (_scratchArrays == null) 
     { 
      _scratchArrays = new Dictionary<int,double[]>(); 
     } 
     return _scratchArrays; 
    } 
} 

/// Round up to next higher power of 2 (return x if it's already a power of 2). 
public static int Pow2RoundUp (int x) 
{ 
    if (x < 0) 
     return 0; 
    --x; 
    x |= x >> 1; 
    x |= x >> 2; 
    x |= x >> 4; 
    x |= x >> 8; 
    x |= x >> 16; 
    return x+1; 
} 
private static double[] GetScratchArray(int size) 
{ 
    int pow2 = Pow2RoundUp(size); 
    if (!scratchArrays.ContainsKey(pow2)) 
    { 
     scratchArrays.Add(pow2, new double[pow2]); 
    } 
    return scratchArrays[pow2]; 
} 
+0

Purtroppo, Disposed! = Raccolto :(Vedi http://stackoverflow.com/questions/655902/using-and-garbage-collezione –

+0

Questo è un peccato, aggiornerò la mia risposta con un'altra idea. –

+0

Ah, non l'ho fatto So di ThreadStatic, che rende molto più semplice la memoria di scratch locale thread- –

7

Sono solidale con la vostra situazione; quando lavoravo a Roslyn abbiamo considerato con molta attenzione i potenziali problemi di prestazioni causati dalla pressione di raccolta dall'assegnazione di array di lavoro temporanei. La soluzione su cui ci siamo basati è stata una strategia di pooling.

In un compilatore le dimensioni dell'array tendono ad essere piccole e quindi ripetute frequentemente. Nella tua situazione, se hai array di grandi dimensioni, allora quello che farei è seguire il suggerimento di Tom: semplificare il problema di gestione e sprecare spazio. Quando si chiede al pool una serie di dimensioni x, arrotondare x fino alla potenza più vicina di due e allocare un array di tale dimensione, o prenderne uno dal pool. Il chiamante ottiene un array un po 'troppo grande, ma possono essere scritti per affrontarlo. Non dovrebbe essere troppo difficile per cercare nel pool una matrice della dimensione appropriata. Oppure è possibile mantenere un gruppo di pool, un pool per array di dimensioni 1024, uno per 2048 e così via.

La scrittura di un pool di thread sicuro non è troppo difficile, oppure è possibile rendere statico il thread del pool e disporre di un pool per thread.

Il difficile è recuperare la memoria in piscina. Ci sono un paio di modi per affrontarlo. In primo luogo, potresti semplicemente richiedere all'utente l'utilizzo della memoria in pool per chiamare un metodo "back in the pool" quando hanno finito con l'array, se non vogliono assumersi le spese della pressione di raccolta.

Un altro modo è scrivere un wrapper di facciata attorno all'array, renderlo implementabile IDisposable in modo da poter utilizzare "using" (*) e creare un finalizzatore su ciò che riporta l'oggetto nel pool, resuscitandolo. (Assicurati che il finalizzatore ritorni sul bit "I need to be finalized".) I finalizzatori che fanno la resurrezione mi rendono nervoso; Preferirei personalmente l'approccio precedente, che è quello che abbiamo fatto a Roslyn.


(*) Sì, questo viola il principio che "usando" dovrebbe indicare che una risorsa non gestita viene restituito al sistema operativo. Essenzialmente trattiamo la memoria gestita come una risorsa non gestita facendo la nostra gestione, quindi non è così male.

+0

Grazie, questo è esattamente il tipo di storia di guerra che speravo :) Una piscina sembra la soluzione più ragionevole finora, e io Non importa "sprecare" il ricordo come questo. Non sono ancora super venduto per aver affidato agli utenti il ​​compito di rilanciare la risorsa, o di provare a inserirla in un metodo Dispose, ma immagino che sia quello che è. –