Sviluppo di memorie Key / Value per il moderno C ++

9

Sto sviluppando un server di database simile a Cassandra.

Lo sviluppo è iniziato in C, ma le cose sono diventate molto complicate senza classi.

Attualmente ho portato tutto in C ++ 11, ma sto ancora imparando il C ++ "moderno" e dubito di molte cose.

Il database funzionerà con coppie chiave / valore. Ogni coppia ha qualche informazione in più - quando viene creata anche quando scadrà (0 se non scade). Ogni coppia è immutabile.

La chiave è una stringa C, Value is void *, ma almeno per il momento sto operando anche con il valore come stringa C.

Ci sono classi IList astratte. È ereditato da tre classi

  • VectorList - Array dinamico C - simile a std :: vector, ma utilizza realloc
  • LinkList - creato per il confronto tra controlli e prestazioni
  • SkipList : la classe che verrà infine utilizzata.

In futuro potrei fare anche Red Black albero.

Ogni IList contiene zero o più puntatori a coppie, ordinati per chiave.

Se IList è diventato troppo lungo, può essere salvato sul disco in un file speciale. Questo file speciale è di tipo read only list .

Se hai bisogno di cercare un tasto,

  • prima viene cercata la memoria IList ( SkipList , SkipList o LinkList ).
  • Quindi la ricerca viene inviata ai file ordinati per data
    (file più nuovo prima, file più vecchio - ultimo).
    Tutti questi file sono salvati in memoria.
  • Se non viene trovato nulla, la chiave non viene trovata.

Non ho dubbi sull'implementazione di IList cose.

Quello che attualmente mi sta sconcertando è il seguente:

Le coppie hanno dimensioni diverse , sono allocate da new() e hanno std::shared_ptr puntate su di esse.

class Pair{
public:
    // several methods...
private:
    struct Blob;

    std::shared_ptr<const Blob> _blob;
};

struct Pair::Blob{
    uint64_t    created;
    uint32_t    expires;
    uint32_t    vallen;
    uint16_t    keylen;
    uint8_t     checksum;
    char        buffer[2];
};

La variabile membro "buffer" è quella con dimensioni diverse. Memorizza il valore chiave +.
Per esempio. se la chiave è di 10 caratteri e il valore è di altri 10 byte, l'intero oggetto sarà sizeof(Pair::Blob) + 20 (il buffer ha una dimensione iniziale di 2, a causa di due byte di terminazione null)

Questo stesso layout è usato anche sul disco, quindi posso fare qualcosa del genere:

// get the blob
Pair::Blob *blob = (Pair::Blob *) & mmaped_array[pos];

// create the pair, true makes std::shared_ptr not to delete the memory,
// since it does not own it.
Pair p = Pair(blob, true);

// however if I want the Pair to own the memory,
// I can copy it, but this is slower operation.
Pair p2 = Pair(blob);

Tuttavia questa diversa dimensione è un problema in molti posti con codice C ++.

Ad esempio, non posso usare std::make_shared() . Questo è importante per me, perché se ho coppie 1M, avrei allocazioni 2M.

Dall'altro lato, se faccio "buffer" all'array dinamico (es. nuovo char [123]), perderò mmap "trucco", avrò due dereferenze se voglio controllare la chiave e lo farò aggiungi un puntatore singolo - 8 byte alla classe.

Ho anche provato a "tirare" tutti i membri da Pair::Blob a Pair , quindi Pair::Blob è solo il buffer, ma quando l'ho testato, era piuttosto lento, probabilmente a causa della copia dei dati dell'oggetto intorno .

Un altro cambiamento che sto pensando è di rimuovere Pair class e sostituirlo con std::shared_ptr e di "spingere" tutti i metodi nuovamente a Pair::Blob , ma questo non mi aiuterà con la classe di% var% di% della variabile.

Mi chiedo come posso migliorare il design degli oggetti per essere più amichevole con il C ++.

Il codice sorgente completo è qui:
link

    
posta Nick 10.08.2015 - 23:34
fonte

1 risposta

3

L'approccio che raccomanderei è di concentrarsi sull'interfaccia del tuo negozio di valori-chiave, in modo da renderlo il più pulito possibile e il più restrittivo possibile, nel senso che dovrebbe consentire la massima libertà ai chiamanti, ma anche il massimo libertà per scegliere come implementarlo.

Quindi, ti suggerisco di fornire una versione il più semplice possibile e l'implementazione più pulita possibile, senza alcun problema di prestazioni. A me sembra che unordered_map dovrebbe essere la tua prima scelta, o forse map se un'interfaccia deve mostrare una sorta di ordinamento delle chiavi.

Quindi, per prima cosa fallo funzionare in modo pulito e minimale; quindi, mettilo in uso in un'applicazione reale; così facendo, troverai i problemi che devi affrontare nell'interfaccia; quindi, vai avanti e affrontali. La maggior parte delle possibilità è che, come risultato della modifica dell'interfaccia, è necessario riscrivere grandi parti dell'implementazione, quindi ogni volta che hai già investito sulla prima iterazione dell'implementazione oltre la quantità minima di tempo necessaria per ottenere semplicemente a malapena il lavoro è tempo perso.

Quindi, tracciarlo e vedere cosa deve essere migliorato nell'implementazione, senza alterare l'interfaccia. Oppure potresti avere le tue idee su come migliorare l'implementazione, prima ancora del tuo profilo. Va bene, ma è ancora nessun motivo per lavorare su queste idee in qualsiasi momento precedente.

Dici che speri di fare meglio di map ; ci sono due cose che possono essere dette a riguardo:

a) probabilmente non lo farai;

b) evitare l'ottimizzazione prematura a tutti i costi.

Riguardo all'implementazione, il tuo problema principale sembra essere l'allocazione della memoria, dal momento che sembra che tu ti preoccupi di come strutturare il tuo progetto in modo da aggirare i problemi che prevedi di avere riguardo all'allocazione della memoria . Il modo migliore per risolvere i problemi di allocazione della memoria in C ++ consiste nell'implementare un'adeguata gestione dell'allocazione della memoria, non torcendo e piegando il progetto attorno ad essi. Dovresti considerarti fortunato che stai usando C ++, che ti permette di gestire la tua allocazione della memoria, al contrario di linguaggi come Java e C #, in cui sei praticamente bloccato con ciò che il runtime della lingua ha da offrire.

Ci sono vari modi per gestire la memoria in C ++ e la possibilità di sovraccaricare l'operatore new può tornare utile. Un semplicistico allocatore di memoria per il tuo progetto prealloca una vasta gamma di byte e lo usa come un mucchio. ( byte* heap .) Avresti un indice firstFreeByte , inizializzato a zero, che indica il primo byte libero nell'heap. Quando arriva una richiesta di N di byte, si restituisce l'indirizzo heap + firstFreeByte e si aggiunge N a firstFreeByte . Quindi, l'allocazione della memoria diventa così veloce ed efficiente che non diventa praticamente nessun problema.

Ovviamente, la preallocazione di tutta la tua memoria potrebbe non essere una buona idea, quindi potresti dover rompere il tuo heap in banche che sono allocate su richiesta, e continuare a servire richieste di allocazione dal più recente momento banca.

Poiché i tuoi dati sono immutabili, questa è una buona soluzione. Ti consente di abbandonare l'idea di oggetti a lunghezza variabile e di fare in modo che ogni Pair contenga un puntatore ai suoi dati come dovrebbe, poiché l'allocazione di memoria aggiuntiva per i dati non costa praticamente nulla.

Se vuoi essere in grado di scartare gli oggetti dall'heap, in modo da poter recuperare la loro memoria, le cose diventano più complicate: dovrai usare non i puntatori, ma i puntatori ai puntatori, in modo che tu possa sposta sempre gli oggetti attorno agli heap in modo da recuperare lo spazio degli oggetti eliminati. Tutto diventa un po 'più lento a causa dell'extradection aggiuntivo, ma tutto è ancora fulmineo rispetto all'utilizzo delle routine di allocazione della memoria della libreria di runtime standard.

Ma ovviamente è davvero inutile preoccuparsi se non si crea prima una versione semplice, minimale e funzionante del proprio database, e la si può usare in un'applicazione reale.

    
risposta data 06.12.2015 - 13:24
fonte

Leggi altre domande sui tag