Quale struttura di dati dovrei usare per questa strategia di memorizzazione nella cache?


11

Sto lavorando su un'applicazione .NET 4.0, che esegue un calcolo piuttosto costoso su due doppie restituendo una doppia. Questo calcolo viene eseguito per ciascuna delle diverse migliaia di articoli . Questi calcoli vengono eseguiti in un Taskthread threadpool.

Alcuni test preliminari hanno dimostrato che gli stessi calcoli vengono eseguiti più e più volte, quindi vorrei memorizzare nella cache n risultati. Quando la cache è piena, vorrei buttare via il meno- spesso oggetto utilizzato di recente. ( Modifica: mi sono reso conto meno spesso che non ha senso, perché quando la cache è piena e sostituivo un risultato con uno appena calcolato, quello sarebbe usato meno spesso e immediatamente sostituito la prossima volta che verrà calcolato un nuovo risultato e aggiunto alla cache)

Per implementare questo, stavo pensando di usare un Dictionary<Input, double>(dove Inputsarebbe una mini-classe che memorizza i due valori di input doppio) per memorizzare gli input e i risultati memorizzati nella cache. Tuttavia, avrei anche bisogno di tenere traccia di quando è stato utilizzato un risultato l'ultima volta. Per questo penso che avrei bisogno di una seconda raccolta che memorizzi le informazioni di cui avrei bisogno per rimuovere un risultato dal dittonario quando la cache si stava riempiendo. Sono preoccupato che mantenere costantemente ordinato questo elenco avrebbe un impatto negativo sulle prestazioni.

Esiste un modo migliore (ovvero più performante) per farlo, o forse anche una struttura di dati comune di cui non sono a conoscenza? Che tipo di cose dovrei profilare / misurare per determinare l'ottimalità della mia soluzione?

Risposte:


12

Se vuoi usare una cache di sfratto LRU (sfratto meno usato di recente), probabilmente una buona combinazione di strutture di dati da usare è:

  • Elenco collegato circolare (come coda prioritaria)
  • Dizionario

Ecco perché:

  • L'elenco collegato ha un tempo di inserimento e rimozione O (1)
  • I nodi dell'elenco possono essere riutilizzati quando l'elenco è pieno e non è necessario eseguire allocazioni aggiuntive.

Ecco come dovrebbe funzionare l'algoritmo di base:

Le strutture dati

LinkedList<Node<KeyValuePair<Input,Double>>> list; Dictionary<Input,Node<KeyValuePair<Input,Double>>> dict;

  1. L'ingresso è ricevuto
  2. Se il dizionario contiene la chiave
    • restituisce il valore memorizzato nel nodo e sposta il nodo all'inizio dell'elenco
  3. Se il dizionario non contiene la chiave
    • calcola il valore
    • memorizzare il valore nell'ultimo nodo dell'elenco
    • se l'ultimo non ha un valore, rimuovi la chiave precedente dal dizionario
    • sposta l'ultimo nodo in prima posizione.
    • memorizza nel dizionario la coppia valore-chiave (input, nodo).

Alcuni vantaggi di questo approccio sono, la lettura e l'impostazione di un valore del dizionario si avvicina a O (1), l'inserimento e la rimozione di un nodo in un elenco collegato è O (1), il che significa che l'algoritmo si avvicina a O (1) per la lettura e la scrittura di valori nella cache ed evita le allocazioni di memoria e blocca le operazioni di copia della memoria, rendendola stabile dal punto di vista della memoria.


Aspetti positivi, l'idea migliore finora, IMHO. Ho implementato una cache basata su questo oggi e dovrò profilare e vedere come si comporta bene domani.
PersonalNexus,

3

Questo sembra un grande sforzo da compiere per un singolo calcolo, data la potenza di elaborazione che hai a disposizione nel PC medio. Inoltre, avrai comunque la spesa della prima chiamata al tuo calcolo per ogni coppia di valori univoca, quindi 100.000 coppie di valori univoci ti costeranno comunque un tempo n * 100.000 al minimo. Considera che l'accesso ai valori nel tuo dizionario probabilmente rallenterà man mano che il dizionario diventa più grande. Potete garantire che la vostra velocità di accesso al dizionario compensi abbastanza da fornire un ritorno ragionevole rispetto alla velocità del vostro calcolo?

Indipendentemente da ciò, sembra che probabilmente dovrai considerare di trovare un modo per ottimizzare il tuo algoritmo. Per questo avrai bisogno di uno strumento di profilazione, come Redgate Ants per vedere dove si trovano i colli di bottiglia e per aiutarti a determinare se ci sono modi per ridurre alcune delle spese generali che potresti avere relative a istanze di classe, attraversamenti di elenchi, database accessi o qualunque cosa ti stia costando così tanto tempo.


1
Sfortunatamente, per ora l'algoritmo di calcolo non può essere modificato, in quanto si tratta di una libreria di terze parti che utilizza una matematica avanzata che è naturalmente ad alta intensità di CPU. Se in un secondo momento verrà rielaborato, verificherò sicuramente gli strumenti di profilazione suggeriti. Inoltre, il calcolo verrà eseguito abbastanza spesso, a volte con input identici, quindi la profilazione preliminare ha mostrato un chiaro vantaggio anche con una strategia di cache molto ingenua.
PersonalNexus,

0

Un pensiero è perché solo cache n risultati? Anche se n è 300.000, useresti solo 7,2 MB di memoria (più qualsiasi extra per la struttura della tabella). Ciò presuppone ovviamente tre doppi a 64 bit. È possibile semplicemente applicare la memoizzazione alla complessa routine di calcolo se non si è preoccupati di esaurire lo spazio di memoria.


Non ci sarà solo una cache, ma una per "elemento" che sto analizzando e possono esserci diverse centinaia di migliaia di questi elementi.
PersonalNexus,

In che modo importa da quale "elemento" proviene l'input? ci sono effetti collaterali?
jk.

@jk. Articoli diversi producono input molto diversi per il calcolo. Dal momento che ciò significa che ci saranno poche sovrapposizioni, non penso che tenerle in una singola cache abbia senso. Inoltre, oggetti diversi potrebbero vivere in thread diversi, quindi per evitare lo stato condiviso, vorrei mantenere separate le cache.
PersonalNexus,

@PersonalNexus Prendo questo per implicare che ci sono più di 2 parametri coinvolti nel calcolo? Altrimenti, in pratica hai ancora f (x, y) = fai qualcosa. Inoltre lo stato condiviso sembra aiutare le prestazioni piuttosto che ostacolarlo?
Peter Smith,

@PeterSmith I due parametri sono gli ingressi principali. Ce ne sono altri, ma raramente cambiano. Se lo facessero, getterei via l'intera cache. Per "stato condiviso" intendevo una cache condivisa per tutti o un gruppo di elementi. Poiché ciò dovrebbe essere bloccato o sincronizzato in altro modo, ciò ostacolerebbe le prestazioni. Ulteriori informazioni sulle implicazioni delle prestazioni dello stato condiviso .
PersonalNexus,

0

L'approccio con la seconda collezione va bene. Dovrebbe essere una coda di priorità che consente di trovare / eliminare rapidamente i valori minimi e anche di cambiare (aumentare) le priorità all'interno della coda (quest'ultima parte è quella difficile, non supportata dalla maggior parte delle semplici implementazioni della coda di prio). La libreria C5 ha una tale raccolta, si chiama IntervalHeap.

Oppure, puoi provare a costruire la tua collezione, qualcosa come un SortedDictionary<int, List<InputCount>>. ( InputCountdeve essere una classe che combina i tuoi Inputdati con il tuo Countvalore)

L'aggiornamento di tale raccolta quando si modifica il valore del conteggio può essere implementato rimuovendo e reinserendo un elemento.


0

Come sottolineato nella risposta di Peter Smith, il modello che si sta tentando di implementare si chiama memoization . In C # è piuttosto difficile implementare la memoizzazione in modo trasparente senza effetti collaterali. Il libro di Oliver Sturm sulla programmazione funzionale in C # fornisce una soluzione (il codice è disponibile per il download, capitolo 10).

In F # sarebbe molto più semplice. Certo, è una grande decisione iniziare a usare un altro linguaggio di programmazione, ma può valere la pena considerare. Soprattutto nei calcoli complessi, è destinato a rendere più facili da programmare più della memoizzazione.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.