Esiste una documentazione che confronta / contrappone le implementazioni della libreria standard C ++? [chiuso]


16

(Questa non è di per sé una programmazione di gioco, ma sono certo che se avessi chiesto questo su SO mi avrebbero detto di non ottimizzare prematuramente, anche se la storia ci dice che ogni gioco di grandi dimensioni finisce per preoccuparsi di queste cose.)

Esiste un documento che riassuma le differenze nelle prestazioni, e in particolare nell'uso della memoria, tra le diverse implementazioni di librerie standard C ++? I dettagli di alcune implementazioni sono protetti da NDA, ma un confronto tra persino STLport vs libstdc ++ vs libc ++ vs MSVC / Dinkumware (vs. EASTL?) Sembra che sarebbe immensamente utile.

In particolare cerco risposte a domande come:

  • Quanta memoria overhead è associata ai contenitori standard?
  • Quali contenitori, se ce ne sono, eseguono allocazioni dinamiche semplicemente dichiarandosi?
  • Std :: string fa copy-on-write? Ottimizzazione delle stringhe corte? Corde?
  • Std :: deque usa un ring buffer o è una schifezza?

Avevo l'impressione che dequefosse sempre implementata nella STL con un vettore.
Tetrad,

@Tetrad: Fino a poche settimane fa lo ero anche io, ma poi ho letto che era spesso implementato da una struttura simile a una corda - e quello sembra essere ciò che è in STLport.

L'STL ha una bozza di lavoro aperta , che può essere utilizzata per trovare informazioni riguardanti le varie strutture di dati (sia sequenziali che associative), algoritmi e classi di supporto implementate. Tuttavia, sembra che il sovraccarico di memoria sia specifico dell'implementazione, piuttosto che definito dalle specifiche.
Thomas Russell,

3
@Duck: lo sviluppo del gioco è l'unico posto di cui sono a conoscenza che utilizza regolarmente funzionalità C ++ di alto livello, ma ha bisogno di tenere traccia meticolosamente delle allocazioni di memoria perché funziona su sistemi senza memoria virtuale a bassa memoria. Ogni singola risposta su SO sarebbe "non ottimizzare prematuramente, lo STL va bene, usalo!" - Il 50% delle risposte qui finora è questo - e tuttavia il test di Maik mostra chiaramente una grande preoccupazione per i giochi che desiderano usare std :: map, e anche la confusione di Tetrad e le mie sulle comuni implementazioni std :: deque.

2
@Joe Wreschnig Non voglio davvero votare per chiudere perché sono interessato al risultato di questo. : p
L'anatra comunista

Risposte:


6

Nel caso in cui non si trovi un simile grafico di confronto, l'alternativa è iniettare un proprio allocatore nelle classi STL in questione e aggiungere un po 'di registrazione.

L'implementazione che ho testato (VC 8.0) non utilizza allocazione di memoria solo dichiarando una stringa / vettore / deque, ma per esso elenca e mappa. La stringa ha un'ottimizzazione della stringa breve, poiché l'aggiunta di 3 caratteri non ha innescato un'allocazione. L'output viene aggiunto sotto il codice.

// basic allocator implementation used from here
// http://www.codeguru.com/cpp/cpp/cpp_mfc/stl/article.php/c4079

#include <iostream>
#include <iomanip>
#include <string>
#include <vector>
#include <deque>
#include <list>
#include <map>

template <class T> class my_allocator;

// specialize for void:
template <> 
class my_allocator<void> 
{
public:
    typedef void*       pointer;
    typedef const void* const_pointer;
    // reference to void members are impossible.
    typedef void value_type;
    template <class U> 
    struct rebind 
    { 
        typedef my_allocator<U> other; 
    };
};

#define LOG_ALLOC_SIZE(call, size)      std::cout << "  " << call << "  " << std::setw(2) << size << " byte" << std::endl

template <class T> 
class my_allocator 
{
public:
    typedef size_t    size_type;
    typedef ptrdiff_t difference_type;
    typedef T*        pointer;
    typedef const T*  const_pointer;
    typedef T&        reference;
    typedef const T&  const_reference;
    typedef T         value_type;
    template <class U> 
    struct rebind 
    { 
        typedef my_allocator<U> other; 
    };

    my_allocator() throw() : alloc() {}
    my_allocator(const my_allocator&b) throw() : alloc(b.alloc) {}

    template <class U> my_allocator(const my_allocator<U>&b) throw() : alloc(b.alloc) {}
    ~my_allocator() throw() {}

    pointer       address(reference x) const                    { return alloc.address(x); }
    const_pointer address(const_reference x) const              { return alloc.address(x); }

    pointer allocate(size_type s, 
               my_allocator<void>::const_pointer hint = 0)      { LOG_ALLOC_SIZE("my_allocator::allocate  ", s * sizeof(T)); return alloc.allocate(s, hint); }
    void deallocate(pointer p, size_type n)                     { LOG_ALLOC_SIZE("my_allocator::deallocate", n * sizeof(T)); alloc.deallocate(p, n); }

    size_type max_size() const throw()                          { return alloc.max_size(); }

    void construct(pointer p, const T& val)                     { alloc.construct(p, val); }
    void destroy(pointer p)                                     { alloc.destroy(p); }

    std::allocator<T> alloc;
};

int main(int argc, char *argv[])
{

    {
        typedef std::basic_string<char, std::char_traits<char>, my_allocator<char> > my_string;

        std::cout << "===============================================" << std::endl;
        std::cout << "my_string ctor start" << std::endl;
        my_string test;
        std::cout << "my_string ctor end" << std::endl;
        std::cout << "my_string add 3 chars" << std::endl;
        test = "abc";
        std::cout << "my_string add a huge number of chars chars" << std::endl;
        test += "d df uodfug ondusgp idugnösndögs ifdögsdoiug ösodifugnösdiuödofu odsugöodiu niu od unoudö n nodsu nosfdi un abc";
        std::cout << "my_string copy" << std::endl;
        my_string copy = test;
        std::cout << "my_string copy on write test" << std::endl;
        copy[3] = 'X';
        std::cout << "my_string dtors start" << std::endl;
    }

    {
        std::cout << std::endl << "===============================================" << std::endl;
        std::cout << "vector ctor start" << std::endl;
        std::vector<int, my_allocator<int> > v;
        std::cout << "vector ctor end" << std::endl;
        for(int i = 0; i < 5; ++i)
        {
            v.push_back(i);
        }
        std::cout << "vector dtor starts" << std::endl;
    }

    {
        std::cout << std::endl << "===============================================" << std::endl;
        std::cout << "deque ctor start" << std::endl;
        std::deque<int, my_allocator<int> > d;
        std::cout << "deque ctor end" << std::endl;
        for(int i = 0; i < 5; ++i)
        {
            std::cout << "deque insert start" << std::endl;
            d.push_back(i);
            std::cout << "deque insert end" << std::endl;
        }
        std::cout << "deque dtor starts" << std::endl;
    }

    {
        std::cout << std::endl << "===============================================" << std::endl;
        std::cout << "list ctor start" << std::endl;
        std::list<int, my_allocator<int> > l;
        std::cout << "list ctor end" << std::endl;
        for(int i = 0; i < 5; ++i)
        {
            std::cout << "list insert start" << std::endl;
            l.push_back(i);
            std::cout << "list insert end" << std::endl;
        }
        std::cout << "list dtor starts" << std::endl;
    }

    {
        std::cout << std::endl << "===============================================" << std::endl;
        std::cout << "map ctor start" << std::endl;
        std::map<int, float, std::less<int>, my_allocator<std::pair<const int, float> > > m;
        std::cout << "map ctor end" << std::endl;
        for(int i = 0; i < 5; ++i)
        {
            std::cout << "map insert start" << std::endl;
            std::pair<int, float> a(i, (float)i);
            m.insert(a);
            std::cout << "map insert end" << std::endl;
        }
        std::cout << "map dtor starts" << std::endl;
    }

    return 0;
}

Finora testato VC8 e STLPort 5.2, ecco il confronto (incluso nel test: stringa, vettore, deque, elenco, mappa)

                    Allocation on declare   Overhead List Node      Overhead Map Node

VC8                 map, list               8 Byte                  16 Byte
STLPort 5.2 (VC8)   deque                   8 Byte                  16 Byte
Paulhodge's EASTL   (none)                  8 Byte                  16 Byte

VC8 stringa di output / vettore / deque / list / map:

===============================================
my_string ctor start
my_string ctor end
my_string add 3 chars
my_string add a huge number of chars chars
  my_allocator::allocate    128 byte
my_string copy
  my_allocator::allocate    128 byte
my_string copy on write test
my_string dtors start
  my_allocator::deallocate  128 byte
  my_allocator::deallocate  128 byte

===============================================
vector ctor start
vector ctor end
  my_allocator::allocate     4 byte
  my_allocator::allocate     8 byte
  my_allocator::deallocate   4 byte
  my_allocator::allocate    12 byte
  my_allocator::deallocate   8 byte
  my_allocator::allocate    16 byte
  my_allocator::deallocate  12 byte
  my_allocator::allocate    24 byte
  my_allocator::deallocate  16 byte
vector dtor starts
  my_allocator::deallocate  24 byte

===============================================
deque ctor start
deque ctor end
deque insert start
  my_allocator::allocate    32 byte
  my_allocator::allocate    16 byte
deque insert end
deque insert start
deque insert end
deque insert start
deque insert end
deque insert start
deque insert end
deque insert start
  my_allocator::allocate    16 byte
deque insert end
deque dtor starts
  my_allocator::deallocate  16 byte
  my_allocator::deallocate  16 byte
  my_allocator::deallocate  32 byte

===============================================
list ctor start
  my_allocator::allocate    12 byte
list ctor end
list insert start
  my_allocator::allocate    12 byte
list insert end
list insert start
  my_allocator::allocate    12 byte
list insert end
list insert start
  my_allocator::allocate    12 byte
list insert end
list insert start
  my_allocator::allocate    12 byte
list insert end
list insert start
  my_allocator::allocate    12 byte
list insert end
list dtor starts
  my_allocator::deallocate  12 byte
  my_allocator::deallocate  12 byte
  my_allocator::deallocate  12 byte
  my_allocator::deallocate  12 byte
  my_allocator::deallocate  12 byte
  my_allocator::deallocate  12 byte

===============================================
map ctor start
  my_allocator::allocate    24 byte
map ctor end
map insert start
  my_allocator::allocate    24 byte
map insert end
map insert start
  my_allocator::allocate    24 byte
map insert end
map insert start
  my_allocator::allocate    24 byte
map insert end
map insert start
  my_allocator::allocate    24 byte
map insert end
map insert start
  my_allocator::allocate    24 byte
map insert end
map dtor starts
  my_allocator::deallocate  24 byte
  my_allocator::deallocate  24 byte
  my_allocator::deallocate  24 byte
  my_allocator::deallocate  24 byte
  my_allocator::deallocate  24 byte
  my_allocator::deallocate  24 byte

STLPort 5.2. output compilato con VC8

===============================================
my_string ctor start
my_string ctor end
my_string add 3 chars
my_string add a huge number of chars chars
  my_allocator::allocate    115 byte
my_string copy
  my_allocator::allocate    115 byte
my_string copy on write test
my_string dtors start
  my_allocator::deallocate  115 byte
  my_allocator::deallocate  115 byte

===============================================
vector ctor start
vector ctor end
  my_allocator::allocate     4 byte
  my_allocator::deallocate   0 byte
  my_allocator::allocate     8 byte
  my_allocator::deallocate   4 byte
  my_allocator::allocate    16 byte
  my_allocator::deallocate   8 byte
  my_allocator::allocate    32 byte
  my_allocator::deallocate  16 byte
vector dtor starts
  my_allocator::deallocate  32 byte

===============================================
deque ctor start
  my_allocator::allocate    32 byte
  my_allocator::allocate    128 byte
deque ctor end
deque insert start
deque insert end
deque insert start
deque insert end
deque insert start
deque insert end
deque insert start
deque insert end
deque insert start
deque insert end
deque dtor starts
  my_allocator::deallocate  128 byte
  my_allocator::deallocate  32 byte

===============================================
list ctor start
list ctor end
list insert start
  my_allocator::allocate    12 byte
list insert end
list insert start
  my_allocator::allocate    12 byte
list insert end
list insert start
  my_allocator::allocate    12 byte
list insert end
list insert start
  my_allocator::allocate    12 byte
list insert end
list insert start
  my_allocator::allocate    12 byte
list insert end
list dtor starts
  my_allocator::deallocate  12 byte
  my_allocator::deallocate  12 byte
  my_allocator::deallocate  12 byte
  my_allocator::deallocate  12 byte
  my_allocator::deallocate  12 byte

===============================================
map ctor start
map ctor end
map insert start
  my_allocator::allocate    24 byte
map insert end
map insert start
  my_allocator::allocate    24 byte
map insert end
map insert start
  my_allocator::allocate    24 byte
map insert end
map insert start
  my_allocator::allocate    24 byte
map insert end
map insert start
  my_allocator::allocate    24 byte
map insert end
map dtor starts
  my_allocator::deallocate  24 byte
  my_allocator::deallocate  24 byte
  my_allocator::deallocate  24 byte
  my_allocator::deallocate  24 byte
  my_allocator::deallocate  24 byte

Risultati EASTL , nessun deque disponibile

===============================================
my_string ctor start
my_string ctor end
my_string add 3 chars
  my_allocator::allocate     9 byte
my_string add a huge number of chars chars
  my_allocator::allocate    115 byte
  my_allocator::deallocate   9 byte
my_string copy
  my_allocator::allocate    115 byte
my_string copy on write test
my_string dtors start
  my_allocator::deallocate  115 byte
  my_allocator::deallocate  115 byte

===============================================
vector ctor start
vector ctor end
  my_allocator::allocate     4 byte
  my_allocator::allocate     8 byte
  my_allocator::deallocate   4 byte
  my_allocator::allocate    16 byte
  my_allocator::deallocate   8 byte
  my_allocator::allocate    32 byte
  my_allocator::deallocate  16 byte
vector dtor starts
  my_allocator::deallocate  32 byte

===============================================
list ctor start
list ctor end
list insert start
  my_allocator::allocate    12 byte
list insert end
list insert start
  my_allocator::allocate    12 byte
list insert end
list insert start
  my_allocator::allocate    12 byte
list insert end
list insert start
  my_allocator::allocate    12 byte
list insert end
list insert start
  my_allocator::allocate    12 byte
list insert end
list dtor starts
  my_allocator::deallocate  12 byte
  my_allocator::deallocate  12 byte
  my_allocator::deallocate  12 byte
  my_allocator::deallocate  12 byte
  my_allocator::deallocate  12 byte

===============================================
map ctor start
map ctor end
map insert start
  my_allocator::allocate    24 byte
map insert end
map insert start
  my_allocator::allocate    24 byte
map insert end
map insert start
  my_allocator::allocate    24 byte
map insert end
map insert start
  my_allocator::allocate    24 byte
map insert end
map insert start
  my_allocator::allocate    24 byte
map insert end
map dtor starts
  my_allocator::deallocate  24 byte
  my_allocator::deallocate  24 byte
  my_allocator::deallocate  24 byte
  my_allocator::deallocate  24 byte
  my_allocator::deallocate  24 byte

Ciò è utile per ottenere i dettagli delle allocazioni sottostanti, ma purtroppo non ci dice nulla sull'overhead e sulle prestazioni della cache previste.

@Giovanni, è difficile rispondere a tutte le tue domande in un'unica risposta. Non sono sicuro di cosa significhi esattamente con "spese generali" e inoltre, rispetto a cosa? Ho pensato per overhead che vuoi dire il consumo di memoria.
Maik Semder,

Per "overhead" intendevo più le dimensioni di un'istanza vuota delle strutture e di tutti i loro iteratori associati e come le più complicate gestiscono l'allocazione - ad esempio std :: list alloca internamente più di un nodo alla volta, oppure pagare il costo di allocazione di base per ciascun nodo, ecc.?

1
La domanda non è tanto "Ti preghiamo di fare questo confronto" quanto di "dove è una risorsa per questo confronto" - Non penso che SO sia un buon posto per "mantenerlo". Forse dovresti iniziare a vomitare su un sito o wiki di Google o qualcosa del genere.

1
@Joe bene ora è qui: p Non sono molto interessato a spostarlo su un altro sito, ero solo interessato ai risultati.
Maik Semder,

8

std::stringnon copia su scrittura. Il CoW era un'ottimizzazione, ma non appena più thread entrano nell'immagine è oltre una pessimizzazione, può rallentare il codice di enormi fattori. È così grave che lo standard C ++ 0x lo vieta attivamente come strategia di implementazione. Non solo, ma la permissività di std::stringrivelare iteratori mutabili e riferimenti a caratteri significa che "scrivere" perstd::string implica quasi ogni operazione.

L'ottimizzazione delle stringhe corte è di circa 6 caratteri, credo, o qualcosa del genere in quella regione. Le corde non sono consentite: è std::stringnecessario memorizzare memoria contigua per la c_str()funzione. Tecnicamente, potresti mantenere sia una corda contigua che una corda della stessa classe, ma nessuno l'ha mai fatto. Inoltre, da quello che so delle corde, renderle sicure da manipolare sarebbe incredibilmente lento, forse peggiore o peggiore di CoW.

Nessun contenitore esegue l'allocazione della memoria essendo dichiarato nei moderni STL. I contenitori basati su nodi come list e map erano soliti farlo ma ora hanno un'ottimizzazione dell'end incorporata e non ne hanno bisogno. È comune eseguire un'ottimizzazione chiamata "swaptimization" in cui si scambia con un contenitore vuoto. Ritenere:

std::vector<std::string> MahFunction();
int main() {
    std::vector<std::string> MahVariable;
    MahFunction().swap(MahVariable);
}

Ovviamente, in C ++ 0x questo è ridondante, ma in C ++ 03 allora quando veniva comunemente usato, se MahVariable alloca memoria sulla dichiarazione, riduce l'efficacia. So per vectorcerto che questo è stato usato per riallocazioni più rapide di contenitori come in MSVC9 STL che ha rimosso la necessità di copiare gli elementi.

dequeutilizza qualcosa indicato come un elenco di collegamenti non srotolati. È fondamentalmente un elenco di matrici, di solito in-node di dimensioni fisse. In quanto tale, per la maggior parte degli usi, mantiene i vantaggi di entrambi i dati structures- accesso contiguo e ammortizzato O (1) rimozione e di essere in grado di aggiungere sia anteriore e posteriore e meglio invalidazione iteratore rispetto vector. dequenon può mai essere implementato dal vettore a causa della sua complessità algoritmica e garanzie di invalidità dell'iteratore.

Quanto overhead di memoria è associato? Bene, onestamente, è una domanda un po 'inutile da porre. I contenitori STL sono progettati per essere efficienti e se si dovesse replicare la loro funzionalità, si finirebbe con qualcosa che funziona peggio o di nuovo nello stesso punto. Conoscendo le loro strutture di dati sottostanti, puoi conoscere l'overhead di memoria che usano, danno o prendono, e sarà solo più di questo per una buona ragione, come l'ottimizzazione delle stringhe di piccole dimensioni.


"È così grave che lo standard C ++ 0x lo vieti attivamente come strategia di implementazione." E lo vietano perché le implementazioni precedenti lo usavano o cercavano di usarlo. Apparentemente vivi in ​​un mondo in cui tutti usano sempre l'ultimo STL implementato in modo ottimale. Questa risposta non è affatto utile.

Sono anche curioso di sapere quali proprietà di std :: deque pensi impediscano una memoria sottostante contigua - gli iteratori sono validi solo dopo le rimozioni all'inizio / alla fine, non nel mezzo né dopo eventuali inserimenti, che possono essere facilmente eseguiti con un vettore. E l'uso di un buffer circolare sembra soddisfare tutte le garanzie algoritmiche - O (1) ammortizzato inserire ed eliminare alle estremità, O (n) eliminare nel mezzo.

3
@Joe: penso che CoW sia stato notato come una cosa negativa dalla fine degli anni '90. Ci sono implementazioni di stringhe che lo hanno usato, in particolare CString, ma ciò non significa che std::stringlo facessero le volte. Per questo non è necessario utilizzare le più recenti e le migliori implementazioni STL. msdn.microsoft.com/en-us/library/22a9t119.aspx dice "Se un elemento è inserito nella parte anteriore, tutti i riferimenti rimangono validi". Non sei sicuro di come intendi implementarlo con un buffer circolare, poiché dovrai ridimensionarlo quando sarà pieno.
DeadMG


Certamente non difenderò il COW come tecnica di implementazione, ma non sono nemmeno ingenuo sulla frequenza con cui il software continua ad essere implementato usando tecniche scadenti per molto tempo dopo che sono state identificate come povere. Ad esempio, il test di Maik sopra rivela uno stdlib moderno che si alloca sulla dichiarazione. Grazie per il puntatore sulla validità del riferimento deque. (Per nitpick, un vettore può soddisfare tutte le garanzie sull'invalidazione dell'iteratore e sulla complessità algoritmica; tale requisito non è nessuno dei due.) Se non altro, lo vedo come ulteriore necessità di un documento come la mia domanda.

2

La domanda non è tanto "Ti preghiamo di fare questo confronto" quanto di "dov'è una risorsa per questo confronto"

Se questa è davvero la tua domanda (che sicuramente non è quella che hai detto nel testo della domanda reale, che è terminata con 4 domande, nessuna delle quali ti chiedeva dove potresti trovare una risorsa), la risposta è semplicemente:

Non ce n'è uno.

La maggior parte dei programmatori C ++ non devono preoccuparsi più di tanto il sovraccarico di strutture della libreria standard, le prestazioni della cache di loro (che è fortemente compilatore comunque dipendente), o cose del genere. Per non parlare, di solito non puoi scegliere l'implementazione della libreria standard; usi ciò che viene fornito con il tuo compilatore. Quindi, anche se fa cose spiacevoli, le opzioni per le alternative sono limitate.

Naturalmente ci sono programmatori a cui interessa questo genere di cose. Ma tutti hanno giurato di usare la libreria standard molto tempo fa.

Quindi hai un gruppo di programmatori a cui semplicemente non importa. E un altro gruppo di programmatori a cui interesserebbe se lo stessero usando, ma dal momento che non lo stanno usando, a loro non importa. Dal momento che nessuno se ne preoccupa, non ci sono informazioni reali su questo genere di cose. Ci sono patch informali di informazioni qua e là (Effective C ++ ha una sezione sulle implementazioni std :: string e le grandi differenze tra loro), ma nulla di esaustivo. E certamente nulla è stato aggiornato.


Risposta speculativa. +1 per probabilmente vero, -1 per nessun modo di dimostrarlo.
Deceleratedcaviar

Ho visto molti paragoni molto buoni e dettagliati in passato, ma sono tutti obsoleti. Al giorno d'oggi qualsiasi cosa prima dell'introduzione della mossa è praticamente irrilevante.
Peter - Unban Robert Harvey,
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.