Gestione della memoria per un rapido passaggio dei messaggi tra i thread in C ++


9

Supponiamo che ci siano due thread, che comunicano inviando in modo asincrono messaggi di dati tra loro. Ogni thread ha un qualche tipo di coda di messaggi.

La mia domanda è di livello molto basso: quale può essere il modo più efficiente di gestire la memoria? Mi vengono in mente diverse soluzioni:

  1. Il mittente crea l'oggetto tramite new. Chiamate del ricevitore delete.
  2. Pool di memoria (per trasferire nuovamente la memoria al mittente)
  3. Garbage collection (ad es. Boehm GC)
  4. (se gli oggetti sono abbastanza piccoli) copia per valore per evitare completamente l'allocazione dell'heap

1) è la soluzione più ovvia, quindi la userò per un prototipo. È probabile che sia già abbastanza buono. Ma indipendentemente dal mio problema specifico, mi chiedo quale sia la tecnica più promettente se stai ottimizzando le prestazioni.

Mi aspetterei che il pool sia teoricamente il migliore, soprattutto perché è possibile utilizzare ulteriori conoscenze sul flusso di informazioni tra i thread. Tuttavia, temo che sia anche il più difficile da ottenere. Un sacco di messa a punto ... :-(

La raccolta dei rifiuti dovrebbe essere abbastanza facile da aggiungere in seguito (dopo la soluzione 1) e mi aspetto che funzioni molto bene. Quindi, immagino che sia la soluzione più pratica se 1) risulta essere troppo inefficiente.

Se gli oggetti sono piccoli e semplici, la copia in base al valore potrebbe essere la più veloce. Tuttavia, temo che imponga inutili limitazioni all'implementazione dei messaggi supportati, quindi desidero evitarlo.

Risposte:


9

Se gli oggetti sono piccoli e semplici, la copia in base al valore potrebbe essere la più veloce. Tuttavia, temo che imponga inutili limitazioni all'implementazione dei messaggi supportati, quindi desidero evitarlo.

Se riesci ad anticipare un limite superiore char buf[256], ad esempio Un'alternativa pratica se non puoi che invoca allocazioni di heap solo in rari casi:

struct Message
{
    // Stores the message data.
    char buf[256];

    // Points to 'buf' if it fits, heap otherwise.
    char* data;
};

3

Dipenderà da come si implementano le code.

Se si utilizza un array (stile round robin) è necessario impostare un limite superiore per le dimensioni per la soluzione 4. Se si utilizza una coda collegata, sono necessari oggetti allocati.

Quindi, il pool di risorse può essere eseguito facilmente quando si sostituisce semplicemente il nuovo ed si elimina con AllocMessage<T>e freeMessage<T>. Il mio suggerimento sarebbe di limitare la quantità di potenziali dimensioni che Tpossono avere e arrotondare quando si allocano calcestruzzo messages.

La raccolta diretta dei rifiuti può funzionare, ma ciò potrebbe causare lunghe pause quando deve raccogliere una grande parte e (credo) funzionerà un po 'peggio di new / delete.


3

Se è in C ++, usa solo uno dei puntatori intelligenti: unique_ptr funzionerebbe bene per te, poiché non eliminerà l'oggetto sottostante fino a quando nessuno avrà un handle su di esso. Si passa l'oggetto ptr al destinatario per valore e non è necessario preoccuparsi di quale thread dovrebbe eliminarlo (nei casi in cui il destinatario non riceve l'oggetto).

Dovresti comunque gestire il blocco tra i thread, ma le prestazioni saranno buone in quanto nessuna memoria viene copiata (solo l'oggetto ptr stesso, che è minuscolo).

Allocare memoria sull'heap non è la cosa più veloce di sempre, quindi il pooling è usato per renderlo molto più veloce. Basta prendere il blocco successivo da un heap pre-dimensionato in un pool, quindi basta usare una libreria esistente per questo.


2
Il blocco di solito è un problema molto più grande della copia della memoria. Sto solo dicendo.
martedì

Quando scrivi unique_ptr, immagino che intendi shared_ptr. Ma mentre non c'è dubbio che l'uso di un puntatore intelligente è buono per la gestione delle risorse, non cambia il fatto che stai usando una qualche forma di allocazione e deallocazione della memoria. Penso che questa domanda sia più di basso livello.
5gon12eder

3

Il più grande successo prestazionale quando si comunica un oggetto da un thread all'altro è il sovraccarico di afferrare un lucchetto. Questo è nell'ordine di diversi microsecondi, che è significativamente più del tempo medio impiegato da una coppia new/ delete(nell'ordine di cento nanosecondi). Le newimplementazioni sane cercano di evitare il blocco a quasi tutti i costi per evitare il loro impatto sulle prestazioni.

Detto questo, vuoi assicurarti di non aver bisogno di afferrare i blocchi quando comunichi gli oggetti da un thread all'altro. Conosco due metodi generali per raggiungere questo obiettivo. Entrambi funzionano solo unidirezionalmente tra un mittente e un destinatario:

  1. Utilizzare un buffer ad anello. Entrambi i processi controllano un puntatore in questo buffer, uno è il puntatore di lettura, l'altro è il puntatore di scrittura.

    • Il mittente prima controlla se c'è spazio per aggiungere un elemento confrontando i puntatori, quindi aggiunge l'elemento, quindi incrementa il puntatore di scrittura.

    • Il ricevitore verifica se c'è un elemento da leggere confrontando i puntatori, quindi legge l'elemento, quindi incrementa il puntatore di lettura.

    I puntatori devono essere atomici poiché sono condivisi tra i thread. Tuttavia, ogni puntatore viene modificato solo da un thread, l'altro deve solo accedere in lettura al puntatore. Gli elementi nel buffer possono essere puntatori stessi, il che consente di ridimensionare facilmente il buffer dell'anello a una dimensione che non blocchi il mittente.

  2. Utilizzare un elenco collegato che contiene sempre almeno un elemento. Il destinatario ha un puntatore al primo elemento, il mittente ha un puntatore all'ultimo elemento. Questi puntatori non sono condivisi.

    • Il mittente crea un nuovo nodo per l'elenco collegato, impostando il nextpuntatore su nullptr. Quindi aggiorna il nextpuntatore dell'ultimo elemento per puntare al nuovo elemento. Infine, memorizza il nuovo elemento nel proprio puntatore.

    • Il ricevitore osserva il nextpuntatore del primo elemento per vedere se sono disponibili nuovi dati. In tal caso, elimina il primo primo elemento, fa avanzare il proprio puntatore per puntare all'elemento corrente e inizia l'elaborazione.

    In questa configurazione, i nextpuntatori devono essere atomici e il mittente deve essere sicuro di non sottovalutare il secondo ultimo elemento dopo aver impostato il nextpuntatore. Il vantaggio è, ovviamente, che il mittente non deve mai bloccare.

Entrambi gli approcci sono molto più veloci di qualsiasi approccio basato sul blocco, ma richiedono un'implementazione attenta per funzionare correttamente. E, naturalmente, richiedono l'atomicità hardware nativa delle scritture / dei carichi dei puntatori; se l' atomic<>implementazione utilizza internamente un blocco, sei praticamente condannato.

Allo stesso modo, se hai diversi lettori e / o scrittori, sei praticamente condannato: potresti provare a elaborare uno schema senza blocco, ma sarà difficile implementarlo al meglio. Queste situazioni sono molto più facili da gestire con un lucchetto. Tuttavia, una volta afferrato un lucchetto, puoi smettere di preoccuparti di new/ deleteperformance.


+1 Devo dare un'occhiata a questa soluzione di buffer ad anello in alternativa alle code simultanee usando i loop CAS. Sembra molto promettente.
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.