Esempi convincenti di allocatori C ++ personalizzati?


176

Quali sono alcuni dei buoni motivi per abbandonare std::allocatoruna soluzione personalizzata? Hai mai incontrato situazioni in cui era assolutamente necessario per correttezza, prestazioni, scalabilità, ecc.? Qualche esempio davvero intelligente?

Gli allocatori personalizzati sono sempre stati una caratteristica della libreria standard di cui non ho avuto molto bisogno. Mi stavo solo chiedendo se qualcuno qui su SO potesse fornire alcuni esempi convincenti per giustificare la loro esistenza.

Risposte:


121

Come ho già detto qui , ho visto l'allocatore STL personalizzato di Intel TBB migliorare significativamente le prestazioni di un'app multithread semplicemente cambiando una singola

std::vector<T>

per

std::vector<T,tbb::scalable_allocator<T> >

(questo è un modo rapido e conveniente di cambiare l'allocatore per usare gli heap di thread-nifty di TBB; vedi pagina 7 in questo documento )


3
Grazie per quel secondo link. L'uso di allocatori per implementare heap a thread privati ​​è intelligente. Mi piace che questo sia un buon esempio di dove gli allocatori personalizzati hanno un chiaro vantaggio in uno scenario che non è limitato alle risorse (embed o console).
Naaff,

7
Il link originale è ora defunto, ma CiteSeer ha il PDF: citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.71.8289
Arto Bendiken

1
Devo chiedere: puoi spostare in modo affidabile un tale vettore in un altro thread? (Immagino di no)
sellibitze,

@sellibitze: Dal momento che i vettori venivano manipolati dall'interno delle attività TBB e riutilizzati attraverso più operazioni parallele e non vi è alcuna garanzia che il thread di lavoro TBB raccolga le attività, concludo che funziona bene. Sebbene si noti che ci sono stati alcuni problemi storici con la liberazione di oggetti TBB creati su un thread in un altro thread (apparentemente un classico problema con i cumuli privati ​​di thread e i modelli di allocazione e deallocazione produttore-consumatore. TBB afferma che l'allocatore evita questi problemi, ma ho visto diversamente Forse risolto nelle versioni più recenti.)
giorno

@ArtoBendiken: il link per il download sul tuo link non sembra essere valido.
einpoklum,

81

Un'area in cui gli allocatori personalizzati possono essere utili è lo sviluppo del gioco, in particolare sulle console di gioco, in quanto hanno solo una piccola quantità di memoria e nessuno scambio. Su tali sistemi si desidera assicurarsi di avere uno stretto controllo su ciascun sottosistema, in modo che un sistema non critico non possa rubare la memoria da uno critico. Altre cose come gli allocatori di pool possono aiutare a ridurre la frammentazione della memoria. Puoi trovare un lungo e dettagliato documento sull'argomento su:

EASTL - Biblioteca di modelli standard di arti elettroniche


14
+1 per il collegamento EASTL: "Tra gli sviluppatori di giochi la debolezza fondamentale [della STL] è il design degli allocatori std, ed è questa debolezza che è stata il principale fattore che ha contribuito alla creazione di EASTL."
Naaff,

65

Sto lavorando su un allocatore mmap che consente ai vettori di utilizzare la memoria da un file mappato in memoria. L'obiettivo è disporre di vettori che utilizzano l'archiviazione direttamente nella memoria virtuale mappata da mmap. Il nostro problema è migliorare la lettura di file molto grandi (> 10 GB) in memoria senza overhead di copia, quindi ho bisogno di questo allocatore personalizzato.

Finora ho lo scheletro di un allocatore personalizzato (che deriva da std :: allocator), penso che sia un buon punto di partenza per scrivere i propri allocatori. Sentiti libero di usare questo codice nel modo che preferisci:

#include <memory>
#include <stdio.h>

namespace mmap_allocator_namespace
{
        // See StackOverflow replies to this answer for important commentary about inheriting from std::allocator before replicating this code.
        template <typename T>
        class mmap_allocator: public std::allocator<T>
        {
public:
                typedef size_t size_type;
                typedef T* pointer;
                typedef const T* const_pointer;

                template<typename _Tp1>
                struct rebind
                {
                        typedef mmap_allocator<_Tp1> other;
                };

                pointer allocate(size_type n, const void *hint=0)
                {
                        fprintf(stderr, "Alloc %d bytes.\n", n*sizeof(T));
                        return std::allocator<T>::allocate(n, hint);
                }

                void deallocate(pointer p, size_type n)
                {
                        fprintf(stderr, "Dealloc %d bytes (%p).\n", n*sizeof(T), p);
                        return std::allocator<T>::deallocate(p, n);
                }

                mmap_allocator() throw(): std::allocator<T>() { fprintf(stderr, "Hello allocator!\n"); }
                mmap_allocator(const mmap_allocator &a) throw(): std::allocator<T>(a) { }
                template <class U>                    
                mmap_allocator(const mmap_allocator<U> &a) throw(): std::allocator<T>(a) { }
                ~mmap_allocator() throw() { }
        };
}

Per utilizzare questo, dichiarare un contenitore STL come segue:

using namespace std;
using namespace mmap_allocator_namespace;

vector<int, mmap_allocator<int> > int_vec(1024, 0, mmap_allocator<int>());

Può essere utilizzato ad esempio per registrare ogni volta che viene allocata la memoria. Ciò che è necessario è la struttura di rebind, altrimenti il ​​contenitore vettoriale utilizza i metodi di allocazione / deallocazione delle superclassi.

Aggiornamento: l'allocatore di mappatura della memoria è ora disponibile su https://github.com/johannesthoma/mmap_allocator ed è LGPL. Sentiti libero di usarlo per i tuoi progetti.


17
Solo un avvertimento, derivante da std :: allocator non è davvero il modo idiomatico di scrivere allocatori. Dovresti invece guardare allocator_traits, che ti consente di fornire il minimo indispensabile di funzionalità, e la classe traits fornirà il resto. Si noti che l'STL utilizza sempre l'allocatore attraverso allocator_traits, non direttamente, quindi non è necessario fare riferimento a allocator_traits da soli Non c'è molto incentivo a derivare da std :: allocator (anche se questo codice può essere un utile punto di partenza, indipendentemente).
Nir Friedman,

25

Sto lavorando con un motore di archiviazione MySQL che utilizza c ++ per il suo codice. Stiamo utilizzando un allocatore personalizzato per utilizzare il sistema di memoria MySQL anziché competere con MySQL per la memoria. Ci consente di assicurarci di utilizzare la memoria come l'utente ha configurato MySQL per l'uso e non "extra".


21

Può essere utile utilizzare allocatori personalizzati per utilizzare un pool di memoria anziché l'heap. Questo è un esempio tra molti altri.

Nella maggior parte dei casi, si tratta sicuramente di un'ottimizzazione prematura. Ma può essere molto utile in determinati contesti (dispositivi integrati, giochi, ecc.).


3
Oppure, quando quel pool di memoria è condiviso.
Anthony,

9

Non ho scritto codice C ++ con un allocatore STL personalizzato, ma posso immaginare un server web scritto in C ++, che utilizza un allocatore personalizzato per la cancellazione automatica dei dati temporanei necessari per rispondere a una richiesta HTTP. L'allocatore personalizzato può liberare tutti i dati temporanei in una volta che la risposta è stata generata.

Un altro possibile caso d'uso per un allocatore personalizzato (che ho usato) è scrivere un unit test per dimostrare che il comportamento di una funzione non dipende da una parte del suo input. L'allocatore personalizzato può riempire l'area di memoria con qualsiasi modello.


5
Sembra che il primo esempio sia il lavoro del distruttore, non dell'allocatore.
Michael Dorst,

2
Se sei preoccupato per il tuo programma a seconda del contenuto iniziale della memoria dall'heap, una corsa veloce (per esempio durante la notte!) In valgrind ti farà sapere in un modo o nell'altro.
cdyson37,

3
@anthropomorphic: il distruttore e l'allocatore personalizzato funzionerebbero insieme, il distruttore verrebbe eseguito per primo, quindi l'eliminazione dell'allocatore personalizzato, che non chiamerà ancora libero (...), ma verrà chiamato libero (...) in seguito, al termine della richiesta. Questo può essere più veloce dell'allocatore predefinito e ridurre la frammentazione dello spazio degli indirizzi.
punti

8

Quando si lavora con GPU o altri coprocessori, a volte è utile allocare strutture di dati nella memoria principale in modo speciale . Questo modo speciale di allocare memoria può essere implementato in un allocatore personalizzato in modo conveniente.

Il motivo per cui l'allocazione personalizzata tramite il runtime dell'acceleratore può essere utile quando si utilizzano gli acceleratori è il seguente:

  1. attraverso un'allocazione personalizzata il runtime dell'acceleratore o il driver viene avvisato del blocco di memoria
  2. inoltre il sistema operativo può assicurarsi che il blocco di memoria allocato sia bloccato nella pagina (alcuni chiamano questa memoria appuntata ), cioè il sottosistema di memoria virtuale del sistema operativo potrebbe non spostare o rimuovere la pagina all'interno o dalla memoria
  3. se 1. e 2. hold e viene richiesto un trasferimento di dati tra un blocco di memoria bloccato in una pagina e un acceleratore, il runtime può accedere direttamente ai dati nella memoria principale poiché sa dove si trova e può essere sicuro che il sistema operativo non lo ha fatto spostalo / rimuovilo
  4. questo salva una copia della memoria che si verificherebbe con la memoria allocata in modo non bloccato: i dati devono essere copiati nella memoria principale in un'area di gestione temporanea bloccata da pagina con l'acceleratore in grado di inizializzare il trasferimento dei dati (tramite DMA )

1
... per non dimenticare i blocchi di memoria allineati alla pagina. Ciò è particolarmente utile se stai parlando con un driver (ad es. Con FPGA tramite DMA) e non vuoi la seccatura e il sovraccarico del calcolo degli offset in-page per le tue liste di DMA.
Jan

7

Sto usando allocatori personalizzati qui; potresti persino dire che doveva aggirare la gestione della memoria dinamica personalizzata.

Contesto: abbiamo sovraccarichi per malloc, calloc, free e le varie varianti di operator new ed delete, e il linker fa felicemente che STL li usi per noi. Questo ci consente di eseguire operazioni come pooling automatico di piccoli oggetti, rilevamento perdite, riempimento allocazione, riempimento gratuito, allocazione di riempimento con sentinelle, allineamento della cache-line per determinati allocati e ritardo gratuito.

Il problema è che stiamo funzionando in un ambiente incorporato - non c'è abbastanza memoria in giro per fare correttamente la contabilità di rilevamento delle perdite per un lungo periodo. Almeno, non nella RAM standard: c'è un altro mucchio di RAM disponibile altrove, attraverso funzioni di allocazione personalizzate.

Soluzione: scrivere un allocatore personalizzato che utilizza l'heap esteso e utilizzarlo solo all'interno dell'architettura di tracciamento delle perdite di memoria ... Tutto il resto viene impostato sui normali sovraccarichi nuovi / eliminati che eseguono il rilevamento delle perdite. Questo evita il tracciamento del tracker stesso (e fornisce anche un po 'di funzionalità di imballaggio extra, conosciamo le dimensioni dei nodi del tracker).

Lo usiamo anche per conservare i dati di profilazione dei costi delle funzioni, per lo stesso motivo; scrivere una voce per ogni chiamata di funzione e ritorno, così come gli switch di thread, può diventare costoso velocemente. L'allocatore personalizzato ci fornisce di nuovo allocati più piccoli in un'area di memoria di debug più ampia.


5

Sto usando un allocatore personalizzato per contare il numero di allocazioni / deallocazioni in una parte del mio programma e misurare il tempo impiegato. Ci sono altri modi per raggiungere questo obiettivo, ma questo metodo è molto conveniente per me. È particolarmente utile poter utilizzare l'allocatore personalizzato solo per un sottoinsieme dei miei contenitori.


4

Una situazione essenziale: quando si scrive codice che deve funzionare oltre i limiti del modulo (EXE / DLL), è essenziale mantenere le allocazioni e le eliminazioni in un solo modulo.

Dove mi sono imbattuto in questa era un'architettura Plugin su Windows. È essenziale, ad esempio, se si passa una stringa std :: string attraverso il limite della DLL, che qualsiasi riallocazione della stringa si verifichi dall'heap da cui proviene, NON dall'heap nella DLL che potrebbe essere diverso *.

* In realtà è più complicato di così, come se si stesse collegando dinamicamente al CRT questo potrebbe funzionare comunque. Ma se ogni DLL ha un collegamento statico al CRT ci si sta dirigendo verso un mondo di dolore, in cui si verificano continuamente errori di allocazione fantasma.


Se si passano oggetti oltre i confini della DLL, è necessario utilizzare l'impostazione DLL (/ MD (d)) multi-thread (Debug) per entrambi i lati. C ++ non è stato progettato pensando al supporto del modulo. In alternativa, è possibile schermare tutto dietro le interfacce COM e utilizzare CoTaskMemAlloc. Questo è il modo migliore per utilizzare interfacce di plugin che non sono associate a un compilatore, STL o fornitore specifico.
gast128

La regola dei vecchi è: non farlo. Non utilizzare i tipi STL nell'API DLL. E non passare responsabilità libera da memoria dinamica oltre i limiti dell'API DLL. Non esiste alcuna ABI C ++, quindi se trattate ogni DLL come un'API C, evitate un'intera classe di potenziali problemi. A spese della "bellezza c ++", ovviamente. O come suggerisce l'altro commento: utilizzare COM. Il semplice C ++ è una cattiva idea.
BitTickler il

3

Un esempio di tempo in cui li ho usati stava lavorando con sistemi embedded molto limitati dalle risorse. Diciamo che hai 2k di RAM libera e che il tuo programma deve usare un po 'di quella memoria. Devi archiviare diciamo 4-5 sequenze da qualche parte che non sono nello stack e inoltre devi avere un accesso molto preciso su dove vengono archiviate queste cose, questa è una situazione in cui potresti voler scrivere il tuo allocatore. Le implementazioni predefinite possono frammentare la memoria, questo potrebbe essere inaccettabile se non si dispone di memoria sufficiente e non è possibile riavviare il programma.

Un progetto a cui stavo lavorando era l'utilizzo di AVR-GCC su alcuni chip a bassa potenza. Abbiamo dovuto memorizzare 8 sequenze di lunghezza variabile ma con un massimo noto. L' implementazione della libreria standard della gestione della memoriaè un involucro sottile attorno a malloc / free che tiene traccia di dove posizionare gli oggetti anteponendo ogni blocco di memoria allocato con un puntatore appena oltre la fine di quel pezzo di memoria allocata. Quando si alloca un nuovo pezzo di memoria, l'allocatore standard deve camminare su ciascuno dei pezzi di memoria per trovare il blocco successivo disponibile dove si adatterà la dimensione della memoria richiesta. Su una piattaforma desktop questo sarebbe molto veloce per questi pochi elementi, ma devi tenere presente che alcuni di questi microcontrollori sono molto lenti e primitivi in ​​confronto. Inoltre, il problema della frammentazione della memoria era un problema enorme che significava che non avevamo altra scelta che adottare un approccio diverso.

Quindi quello che abbiamo fatto è stato implementare il nostro pool di memoria . Ogni blocco di memoria era abbastanza grande da adattarsi alla sequenza più grande di cui avremmo bisogno. Questo ha assegnato in anticipo blocchi di memoria di dimensioni fisse e contrassegnato quali blocchi di memoria erano attualmente in uso. Lo abbiamo fatto mantenendo un numero intero a 8 bit dove ogni bit rappresentava se veniva usato un certo blocco. Abbiamo scambiato l'uso della memoria qui per tentare di rendere l'intero processo più veloce, il che nel nostro caso era giustificato dal momento che spingevamo questo chip microcontrollore vicino alla sua massima capacità di elaborazione.

Ci sono molte altre volte in cui riesco a vedere scrivere il proprio allocatore personalizzato nel contesto dei sistemi embedded, ad esempio se la memoria per la sequenza non è nella RAM principale, come spesso accade su queste piattaforme .



2

Per la memoria condivisa è fondamentale che non solo la testa del contenitore, ma anche i dati in essa contenuti siano archiviati nella memoria condivisa.

L'allocatore di Boost :: Interprocess è un buon esempio. Tuttavia, come puoi leggere qui, questo allone non è sufficiente, per rendere compatibili tutti i contenitori STL della memoria condivisa (a causa dei diversi offset di mappatura in diversi processi, i puntatori potrebbero "rompersi").


2

Qualche tempo fa ho trovato questa soluzione molto utile per me: allocatore C ++ 11 veloce per contenitori STL . Accelera leggermente i contenitori STL su VS2017 (~ 5x) e su GCC (~ 7x). È un allocatore per scopi speciali basato sul pool di memoria. Può essere utilizzato con i contenitori STL solo grazie al meccanismo richiesto.


1

Personalmente uso Loki :: Allocator / SmallObject per ottimizzare l'utilizzo della memoria per piccoli oggetti: mostra una buona efficienza e prestazioni soddisfacenti se devi lavorare con quantità moderate di oggetti veramente piccoli (da 1 a 256 byte). Può essere fino a ~ 30 volte più efficiente dell'allocazione new / delete C ++ standard se parliamo di allocare quantità moderate di piccoli oggetti di dimensioni diverse. Inoltre, esiste una soluzione specifica per VC chiamata "QuickHeap", offre le migliori prestazioni possibili (allocare e deallocare le operazioni basta leggere e scrivere l'indirizzo del blocco allocato / restituito all'heap, rispettivamente fino a 99. (9)% casi - dipende dalle impostazioni e dall'inizializzazione), ma a un costo di un notevole sovraccarico - ha bisogno di due puntatori per estensione e uno in più per ogni nuovo blocco di memoria. E'

Il problema con l'implementazione C ++ nuova / eliminazione standard è che di solito è solo un wrapper per l'allocazione C malloc / free e funziona bene per blocchi di memoria più grandi, come 1024+ byte. Ha un notevole sovraccarico in termini di prestazioni e, a volte, memoria aggiuntiva utilizzata anche per la mappatura. Pertanto, nella maggior parte dei casi gli allocatori personalizzati sono implementati in modo da massimizzare le prestazioni e / o minimizzare la quantità di memoria aggiuntiva necessaria per allocare oggetti piccoli (≤1024 byte).


1

In una simulazione grafica, ho visto allocatori personalizzati utilizzati per

  1. Vincoli di allineamento che std::allocatornon supportano direttamente.
  2. Ridurre al minimo la frammentazione utilizzando pool separati per allocazioni di breve durata (solo questo frame) e di lunga durata.
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.