6

Si supponga che ho il seguente oggettogestione della memoria/cache per gli oggetti costosi in C#

public class MyClass 
{ 
    public ReadOnlyDictionary<T, V> Dict 
    { 
     get 
     { 
      return createDictionary(); 
     } 
    } 
} 

Si supponga che ReadOnlyDictionary è una sola lettura wrapper Dictionary<T, V>.

Il metodo createDictionary richiede molto tempo per il completamento e il dizionario restituito è relativamente grande.

Ovviamente, voglio implementare una sorta di memorizzazione nella cache in modo da poter riutilizzare il risultato di createDictionary ma anche io non voglio abusare di Garbage Collector e utilizzare molta memoria.

Ho pensato di utilizzare WeakReference per il dizionario ma non sono sicuro se questo è l'approccio migliore.

Cosa mi consiglia? Come gestire correttamente il risultato di un metodo costoso che potrebbe essere chiamato più volte?

UPDATE:

Sono interessato a un consiglio per una libreria C# 2.0 (DLL singolo, non-visuale). La libreria potrebbe essere utilizzata in un desktop di un'applicazione Web.

UPDATE 2:

La questione è rilevante per sola lettura oggetti pure. Ho cambiato il valore della proprietà da Dictionary a ReadOnlyDictionary.

UPDATE 3:

Il T è di tipo relativamente semplice (stringa, ad esempio). Il V è una classe personalizzata. Si potrebbe presumere che un'istanza di V sia costosa da creare. Il dizionario potrebbe contenere da 0 a un paio di migliaia di elementi.

È possibile accedere al codice da un singolo thread o da più thread con un meccanismo di sincronizzazione esterno.

sto bene, se il dizionario è GC-ed, quando nessuno lo usa. Sto cercando di trovare un equilibrio tra il tempo (che voglio mettere in cache in qualche modo il risultato di createDictionary) e le spese di memoria (Non voglio tenere memoria occupato più del necessario).

+1

E riguardo l'utilizzo di sistemi esterni? memcached (con scadenza) forse? – eyossi

+0

È per un'applicazione web o desktop? – McGarnagle

+0

@dbaseman si prega di consultare il mio aggiornamento alla domanda. – Bobrovsky

risposta

1

Ci sono quattro principali meccanismi disponibili per voi (pigro è disponibile in 4.0, quindi è alcuna opzione)

  1. inizializzazione pigra
  2. delega virtuale
  3. fantasma
  4. titolare valore

ognuno ha i suoi vantaggi.

suggerisco un valore titolare, che popola il dizionario al primo richiamo del metodo GetValue del titolare. allora puoi usare quel valore per tutto il tempo che vuoi E E 'solo fatto una sola volta E viene fatto solo quando è nel bisogno.

per ulteriori informazioni, vedere martin fowlers page

0

Penso che tu abbia due meccanismi su cui contare per il caching, invece di sviluppare il proprio. Il primo, come tu stesso hai suggerito, era usare un WeakReference e lasciare che il garbage collector decidesse quando liberare questa memoria.

avete un secondo meccanismo - la memoria di paging. Se il dizionario viene creato in un colpo solo, probabilmente verrà memorizzato in una parte più o meno continua dell'heap. Mantieni vivo il dizionario e lascia che Windows lo impagini nel file di scambio se non ne hai bisogno. A seconda del tuo utilizzo (quanto è casuale l'accesso al tuo dizionario), potresti ottenere prestazioni migliori rispetto a WeakReference.

Questo secondo approccio è problematico se si è vicini ai limiti di spazio dell'indirizzo (ciò si verifica solo nei processi a 32 bit).

+1

Ci scusiamo ma un WeakReference verrà pulito non appena nessun altro fa riferimento a questo dizionario che renderà la cache inutilizzabile. Il paging non aiuta poiché l'heap gestito è (quasi) mai cancellato dal momento che il GC tocca abbastanza frequentemente tutti gli heap GC. –

+0

Stai dicendo che i programmi .NET non vengono mai eliminati? – zmbq

+0

Se si stanno facendo assegnazioni frequenti, è probabile che la maggior parte degli heap del GC vengano eliminati. Sebbene ci siano possibilità che tu possa ottenerlo quando hai uno schema di allocazione specifico. Il GC fa uso di GetWriteWatch Api per verificare se una regione di memoria è stata modificata dall'ultima volta in cui è stata visitata, quindi (in teoria) non è necessario toccarla di nuovo. –

3

WeakReference non è una buona soluzione per una cache poiché l'oggetto non sopravviverà al prossimo GC se nessun altro fa riferimento al dizionario. È possibile creare una cache semplice memorizzando il valore creato in una variabile membro e riutilizzarla se non è nulla.

Questo non è thread-safe e in alcune situazioni si finirebbe per creare il dizionario più volte se si dispone di un accesso concurent pesante ad esso. È possibile utilizzare il doppio schema di blocco controllato per proteggersi da questo con un impatto minimo.

Per aiutarti ulteriormente, devi specificare se l'accesso simultaneo è un problema per te e quanta memoria consuma il tuo dizionario e come viene creato. Se ad es. il dizionario è il risultato di una query costosa che potrebbe aiutare a serializzare semplicemente il dizionario su disco e riutilizzarlo finché non è necessario ricrearlo (ciò dipende dalle esigenze specifiche).

Il caching è un'altra parola per perdita di memoria se non si dispone di una politica chiara quando l'oggetto deve essere rimosso dalla cache. Dal momento che stai provando WeakReference presumo che tu non sappia quando esattamente un buon momento sarebbe quello di svuotare la cache.

Un'altra opzione è quella di comprimere il dizionario in una struttura meno affamata di memoria. Quante chiavi ha il tuo dizionario e quali sono i valori?

+0

per favore vedi il mio aggiornamento alla domanda. – Bobrovsky

+0

Tu dici che V è costoso da creare. Il dizionario è sempre completamente accessibile? Altrimenti, puoi semplicemente tenere un elenco di Vs utilizzato di recente nella cache per calcolare il resto. O ancora meglio è possibile creare un dizionario di K, Lazy in cui il valore viene creato per il dizionario solo quando vi si accede. –

+0

È possibile accedere al dizionario in modo arbitrario (non ho dati sulla frequenza con cui si accede). La tua idea di Lazy è interessata, grazie. Non sono sicuro di come influirà su GC, però. – Bobrovsky

1

Sei sicuro è necessario memorizzare nella cache l'intero dizionario?

Da quello che dici, potrebbe essere meglio conservare un elenco di coppie chiave-valore Utilizzato più di recente.

Se la chiave si trova nell'elenco, è sufficiente restituire il valore.

Se non lo è, crea il valore uno (che è presumibilmente più veloce di crearli tutti e utilizza meno memoria) e memorizzalo nell'elenco, rimuovendo così la coppia chiave-valore che non è stata utilizzata la più lunga.

Ecco una semplice implementazione elenco MRU, potrebbe servire come ispirazione:

using System.Collections.Generic; 
using System.Linq; 

internal sealed class MostRecentlyUsedList<T> : IEnumerable<T> 
{ 
    private readonly List<T> items; 
    private readonly int maxCount; 

    public MostRecentlyUsedList(int maxCount, IEnumerable<T> initialData) 
     : this(maxCount) 
    { 
     this.items.AddRange(initialData.Take(maxCount)); 
    } 

    public MostRecentlyUsedList(int maxCount) 
    { 
     this.maxCount = maxCount; 
     this.items = new List<T>(maxCount); 
    } 

    /// <summary> 
    /// Adds an item to the top of the most recently used list. 
    /// </summary> 
    /// <param name="item">The item to add.</param> 
    /// <returns><c>true</c> if the list was updated, <c>false</c> otherwise.</returns> 
    public bool Add(T item) 
    { 
     int index = this.items.IndexOf(item); 

     if (index != 0) 
     { 
      // item is not already the first in the list 
      if (index > 0) 
      { 
       // item is in the list, but not in the first position 
       this.items.RemoveAt(index); 
      } 
      else if (this.items.Count >= this.maxCount) 
      { 
       // item is not in the list, and the list is full already 
       this.items.RemoveAt(this.items.Count - 1); 
      } 

      this.items.Insert(0, item); 

      return true; 
     } 
     else 
     { 
      return false; 
     } 
    } 

    public IEnumerator<T> GetEnumerator() 
    { 
     return this.items.GetEnumerator(); 
    } 

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() 
    { 
     return this.GetEnumerator(); 
    } 
} 

Nel tuo caso, T è una coppia chiave-valore. Tieni Maxcount abbastanza piccolo, in modo che la ricerca rimanga veloce e per evitare un uso eccessivo della memoria. Chiama Aggiungi ogni volta che usi un oggetto.

+0

L'elenco MRU presenta diversi problemi. Per prima cosa dovresti memorizzare nella cache la chiave del dizionario era buona. Altrimenti si fatica a trovare di nuovo la V giusta per una data chiave. Secondo, ci si basa sul confronto predefinito per V, che è l'uguaglianza di riferimento predefinita che è errata. Non si può presumere che V implementa GetHashCode ed Equals correttamente se è usato come valore in un dizionario. Terzo se si memorizzano nella cache molte voci, List.Contains diventa molto lento. Un Hashset potrebbe essere più veloce al costo della memoria aggiuntiva che è necessario per mantenere i V in un HashSet extra. –

+0

So che ha problemi. Ecco perché ho detto "un'implementazione molto semplice dell'elenco MRU, potrebbe servire da ispirazione" e "nel tuo caso, T è una coppia chiave-valore". Il mio punto era quello di dire: forse non hai bisogno di mettere in cache l'intero dizionario, solo oggetti usati di recente. –

+0

Sì, è giusto. Per ottimizzare correttamente dovremmo avere più dati se c'è un problema reale o se stiamo cercando di risolvere un problema. –

1

Un'applicazione deve utilizzare WeakReference come meccanismo di memorizzazione nella cache se la durata utile della presenza di un oggetto nella cache sarà paragonabile alla durata di riferimento dell'oggetto. Supponiamo, per esempio, di avere un metodo che creerà un ReadOnlyDictionary basato sulla deserializzazione di String.Se uno schema di utilizzo comune sarebbe quello di leggere una stringa, creare un dizionario, fare alcune cose con esso, abbandonarlo e ricominciare da capo con un'altra stringa, probabilmente non è l'ideale per lo WeakReference. D'altra parte, se il tuo obiettivo è deserializzare molte stringhe (alcune delle quali saranno uguali) nelle istanze ReadOnlyDictionary, potrebbe essere molto utile se ripetuti tentativi di deserializzare la stessa stringa producono la stessa istanza. Si noti che i risparmi non derivano solo dal fatto che si è dovuto fare il lavoro di costruzione dell'istanza una sola volta, ma anche dai fatti che (1) non sarebbe stato necessario mantenere più istanze in memoria e (2) se le variabili ReadOnlyDictionary si riferiscono alla stessa istanza, possono essere riconosciute come equivalenti senza dover esaminare le istanze stesse. Al contrario, determinare se due distinte istanze di ReadOnlyDictionary equivalessero potrebbe richiedere di esaminare tutti gli elementi di ciascuno. Il codice che dovrebbe fare molti confronti di questo tipo potrebbe trarre vantaggio dall'uso di una cache WeakReference in modo che le variabili che contengono istanze equivalenti di solito mantengano la stessa istanza.

Problemi correlati