È corretto duplicare una memoria di programma per evitare il reset in linea?

1

Ho una funzione time-critical che deve essere eseguita più volte in modo sequenziale.

void task(state_t *state);

Utilizza una quantità relativamente grande di memoria, quindi ho pensato che allocarlo una volta all'inizio e liberarlo alla fine velocizzasse la sequenza di attività.

state_t *state = alloc_state();

task(state);
task(state);
task(state);

free_state(state);

Ma la memoria deve essere resettata (tutta la memoria impostata su 0) tra le attività, che è tanto lenta quanto la sua allocazione. Poiché l'utilizzo della memoria non è critico, ho pensato di poter duplicare lo stato del programma e utilizzare un thread per ripristinarne uno quando l'altro è in uso.

state_t *state_a = alloc_state();
state_t *state_b = alloc_state();
pthread_t thread1, thread2;

while (1) {

  /* running task on state_a */
  pthread_create(&thread1, NULL, task,        (void*) state_a);
  pthread_create(&thread2, NULL, reset_state, (void*) state_b);
  pthread_join(thread1, NULL);
  pthread_join(thread2, NULL);

  /* running task on state_b */
  pthread_create(&thread1, NULL, task,        (void*) state_b);
  pthread_create(&thread2, NULL, reset_state, (void*) state_a);
  pthread_join(thread1, NULL);
  pthread_join(thread2, NULL);
}

free_state(state_a);
free_state(state_b);

Funziona bene, ma lo trovo inelegante. Va bene fare questo? C'è una soluzione più elegante? Inoltre, anche se la memoria non è critica, esiste una soluzione senza il compromesso della memoria di tempo?

    
posta jean-loup 20.12.2018 - 23:22
fonte

3 risposte

3

Is it ok to do this?

Sì, differire il tempo di lavoro non critico per i processi in background è un compromesso onorato nel tempo tra il completamento del lavoro essenziale e il completamento dell'intera attività (pulizia inclusa).

But the memory has to be reset between tasks, which is as slow as allocating it.

L'unica volta che la memoria deve essere completamente resettata è quando la si consegna a un processo non affidabile. È un problema di sicurezza impedire che i segreti vengano comunicati tra processi non sicuri o tra processi sicuri e non sicuri.

Un algoritmo può sempre essere adattato per inizializzare, o reinizializzare le strutture dati mentre opera e solo quanto è necessario per esprimere lo stato corretto. La maggior parte di questo costo dovrà essere pagato comunque poiché l'algoritmo deve comunque impostare lo stato.

Per quanto riguarda le risorse non di memoria come handle di file, socket, handle del dispositivo, ecc ... dovrai gestirli in modo leggermente diverso. Di solito danneggia le prestazioni e l'opportunità di lasciarle aperte a tempo indefinito, ma sono anche più lente a de-allocare. La soluzione che hai suggerito prima di using a background worker to manage their cleanup è un buon compromesso qui.

Is there a more elegant solution?

Questo dipende da cosa intendi per eleganza.

Come per un problema di gestione, c'è una misura della contabilità necessaria per mantenere lo stato. Ogni soluzione scambia proprietà come:

  • quanto libro tiene
  • quando viene tenuta la contabilità
  • chi è responsabile della conservazione del libro
  • quanto è invasiva la conservazione del libro

Soluzioni:

  1. Algoritmo / sistema speciale ben progettato. Semplicemente non puoi diventare più elegante di questo. Per molti problemi è ancora una scatola nera su come ottenere un algoritmo / sistema ben congegnato per un problema specifico, ma lo saprai quando lo usi.

  2. Raccolta dati obsoleti. È difficile da scrivere e correggere. Meglio usare l'offerta linguistica o un'offerta di terze parti indurita dalla battaglia. Come vantaggio collaterale, l'allocazione della memoria è (di solito) veloce. D'altro canto, non vi è alcuna garanzia su quanta memoria verrà utilizzata, quanto rapidamente verrà ripulito e quando / come si manifestano i sovraccarichi.

  3. Pool di memoria / allocatore di memoria personalizzato / deallocator. Se si conoscono le strutture di memoria specifiche che verranno utilizzate dal proprio algoritmo, un gestore di memoria personalizzato può accelerare notevolmente l'allocazione / deallocazione della memoria. Se sai che un thread lo userà alla volta, può essere accelerato ancora di più. Aiuta anche a limitare la memoria totale in volo e aiuta a localizzare la memoria in uso dall'algoritmo.

  4. State quiescent . Ottimo se è necessario condividere i dati tra processi concorrenti, ma accertarsi che sia sanificato. È un'altra forma di Garbage Collector, ma che può essere specializzata e quindi ottimizzata per carichi di lavoro specifici, tra lavoratori specifici senza influenzare o essere influenzati dal sistema più grande.

  5. Passaggio di messaggi. Questa è la tua soluzione essenziale. L'algoritmo principale tratta la memoria utilizzata come un prodotto che viene inviato come un messaggio su una coda a un riciclatore. Il riciclatore tratta la memoria e la rimanda al riutilizzo o la rilascia. Se l'algoritmo principale non ha ricevuto memoria disinfettata, ne allocherà di più.

Buona pratica

Come per qualsiasi tipo di problema riguardante la gestione delle risorse, opterei per un'offerta / tecnica generica che è temprata da battaglie e relativamente non intrusiva. Resistere all'impulso di ottimizzare presto.

Se l'offerta generica non risolve il problema, prima di tutto ottieni qualche profilazione effettiva sul tuo codice. È sorprendente la frequenza con cui altri codici sono responsabili. Se il codice di gestione delle risorse è effettivamente il colpevole, spesso un po 'di ottimizzazione della configurazione risolverà il problema.

    
risposta data 21.12.2018 - 04:37
fonte
2

It works well, but I find it inelegant. Is it ok to do this?

Sì, va bene.

Is there a more elegant solution? Also, even if memory is not critical, is there a solution without the time-memory trade-off?

Gli algoritmi possono essere fatti per inizializzare i propri dati e quindi non richiedono la reimpostazione della memoria. Quindi, l'idea sarebbe quella di trasformare i tuoi algoritmi in modo che abbiano questa proprietà.

Ad esempio, i garbage collector devono identificare gli oggetti vivi rispetto agli oggetti morti. Quindi, un approccio è quello di contrassegnare tutti gli oggetti come morti, quindi tracciare ogni oggetto raggiungibile da qualche insieme di radici (come le variabili globali e le variabili dello stack). Se raggiungono un oggetto pensato come morto, cambiano il proprio segno per vivere (e se raggiungono un oggetto vivo, non devono tracciare più in basso quel percorso). Alla fine tutti gli oggetti visitati sono contrassegnati dal vivo e tutti gli oggetti ancora segnati come morti sono spazzatura. Tuttavia, la fase iniziale di contrassegnare tutti gli oggetti come vivi è costosa, quindi piuttosto che farlo, invertono il senso di vero & false per live vs. dead in ogni raccolta. Poiché gli oggetti morti vengono tutti raccolti, quando la raccolta è terminata, tutti gli oggetti sono contrassegnati dal vivo. Quindi, quel valore per live viene utilizzato come valore iniziale per dead nella prossima raccolta. In una collezione il bit live / dead nell'oggetto è vero per live e nel prossimo è falso significa live. Pertanto, il costoso processo di inizializzazione può essere saltato.

Molti altri algoritmi e approcci inizializzano positivamente la memoria che usano; questi non richiedono una fase di inizializzazione.

Alcuni tengono traccia della memoria libera rispetto alla memoria utilizzata, utilizzando un contatore delle dimensioni, un indice o un puntatore. Sanno che la memoria oltre un certo intervallo non è stata inizializzata. Un file system tiene traccia di quanto è grande ciascun file e ci impedisce di leggere oltre la fine del file. Per questo motivo, non deve inizializzare i blocchi del disco (vale a dire zero) quando si assegna un blocco libero per un file espandibile - il contenuto di quel blocco verrà inizializzato con i dati del file dall'applicazione che scrive il file, quindi, nessuna inizializzazione separata è necessario un passaggio.

    
risposta data 21.12.2018 - 01:47
fonte
1

È possibile creare una memoria copy-on-write. Il modo più semplice per ottenere ciò è forchetta () - un processo separato, eseguire l'attività nel processo figlio, fare in modo che il genitore attenda il completamento figlio, e al termine, terminare e il genitore riprenderà a fork () per il prossimo compito. Se è necessario condividere lo stato tra le attività, è possibile impostare una memoria condivisa o trasferire lo stato condiviso tramite pipe.

C'è anche un modo per avere copy- comportamento in scrittura senza fork () , anche se potrebbe essere un po 'difficile da ottenere.

L'approccio alternativo che puoi intraprendere, sebbene dipenda dall'attività, è quello di ristrutturare la struttura dei dati di stato in modo da non dover modificare i dati esistenti quando si modifica lo stato. Invece, qualsiasi modifica dovrebbe essere fatta aggiungendo una descrizione della modifica allo stato di base. Questa è l'idea di base di MVCC (controllo della concorrenza a più versioni). In questo modo, lo stato di ripristino viene semplicemente creato creando una nuova versione dello stato e puoi liberare la memoria ogni volta.

    
risposta data 20.12.2018 - 23:51
fonte

Leggi altre domande sui tag