Portage de développement de magasin de clés / valeur vers le C ++ moderne


9

Je développe un serveur de base de données similaire à Cassandra.

Le développement a commencé en C, mais les choses sont devenues très compliquées sans cours.

Actuellement, j'ai tout porté en C ++ 11, mais j'apprends toujours le C ++ "moderne" et j'ai des doutes sur beaucoup de choses.

La base de données fonctionnera avec des paires clé / valeur. Chaque paire a plus d'informations - quand est créé aussi quand il expirera (0 sinon expire). Chaque paire est immuable.

La clé est la chaîne C, la valeur est nulle *, mais au moins pour le moment, j'utilise la valeur comme chaîne C également.

Il y a une IListclasse abstraite . Il est hérité de trois classes

  • VectorList - Tableau dynamique C - similaire à std :: vector, mais utilise realloc
  • LinkList - conçu pour les contrôles et la comparaison des performances
  • SkipList - la classe qui sera finalement utilisée.

À l'avenir, je pourrais aussi faire de l' Red Blackarbre.

Chacun IListcontient zéro ou plusieurs pointeurs vers des paires, triés par clé.

S'il IListest devenu trop long, il peut être enregistré sur le disque dans un fichier spécial. Ce fichier spécial est en quelque sorte read only list.

Si vous devez rechercher une clé,

  • la première en mémoire IListest recherchée ( SkipList, SkipListou LinkList).
  • La recherche est ensuite envoyée aux fichiers triés par date
    (le plus récent en premier, le plus ancien en dernier).
    Tous ces fichiers sont mmap-ed en mémoire.
  • Si rien n'est trouvé, la clé n'est pas trouvée.

Je n'ai aucun doute sur la mise en œuvre des IListchoses.


Ce qui me laisse perplexe actuellement, c'est:

Les paires sont de tailles différentes , elles sont allouées par new()et elles les ont std::shared_ptrpointées du doigt.

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 variable membre "buffer" est celle dont la taille est différente. Il stocke la clé + la valeur.
Par exemple, si la clé est de 10 caractères et que la valeur est de 10 octets supplémentaires, l'objet entier sera sizeof(Pair::Blob) + 20(le tampon a une taille initiale de 2, en raison de deux octets de fin nuls)

Cette même disposition est également utilisée sur le disque, donc je peux faire quelque chose comme ceci:

// 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);

Cependant, cette taille différente est un problème sur beaucoup d'endroits avec du code C ++.

Par exemple, je ne peux pas utiliser std::make_shared(). C'est important pour moi, car si j'ai 1 million de paires, j'aurais 2 millions d'allocations.

De l'autre côté, si je fais du "buffer" dans un tableau dynamique (par exemple nouveau char [123]), je perdrai le "truc" mmap, j'aurai deux déréférences si je veux vérifier la clé et j'ajouterai un seul pointeur - 8 octets à la classe.

J'ai aussi essayé de « tirer » tous les membres de Pair::Blobdans Pair, de sorte Pair::Blobà être juste le tampon, mais quand je l' ai testé, il était assez lent, probablement à cause de la copie des données d'objet autour.

Un autre changement Pairauquel je pense également est de supprimer la classe et de la remplacer par std::shared_ptret de "repousser" toutes les méthodes Pair::Blob, mais cela ne m'aidera pas avec la Pair::Blobclasse de taille variable .

Je me demande comment je peux améliorer la conception des objets afin d'être plus convivial en C ++.


Le code source complet est ici:
https://github.com/nmmmnu/HM3


2
Pourquoi n'utilisez-vous pas std::mapou std::unordered_map? Pourquoi les valeurs (associées aux clés) en sont-elles void*? Vous auriez probablement besoin de les détruire à un moment donné; comment quand? Pourquoi n'utilisez-vous pas de modèles?
Basile Starynkevitch

Je n'utilise pas std :: map, car je crois (ou du moins essaye) de faire quelque chose de mieux que std :: map pour le cas actuel. Mais oui, je pense à un moment donné à envelopper std :: map et à vérifier les performances en tant qu'IList également.
Nick

La désallocation et l'appel des d-tors se font là où l'élément est IList::removeou quand IList est détruit. Cela prend beaucoup de temps, mais je vais le faire dans un fil séparé. Ce sera facile car IList le sera std::unique_ptr<IList>quand même. donc je pourrai le "changer" avec une nouvelle liste et garder l'ancien objet quelque part où je pourrai appeler d-tor.
Nick

J'ai essayé des modèles. Ce n'est pas la meilleure solution ici, car ce n'est pas la bibliothèque utilisateur, la clé est toujours C stringet les données sont toujours un tampon void *ou char *, donc vous pouvez passer un tableau char. Vous pouvez trouver similaire dans redisou memcached. À un moment donné, je pourrais décider d'utiliser std::stringou d'un tableau de caractères fixe pour la clé, mais souligner que ce sera toujours la chaîne C.
Nick

6
Au lieu d'ajouter 4 commentaires, vous devriez modifier votre question
Basile Starynkevitch

Réponses:


3

L'approche que je recommanderais est de se concentrer sur l'interface de votre magasin de valeurs-clés, afin de le rendre aussi propre que possible et aussi restrictif que possible, ce qui signifie qu'il devrait permettre une liberté maximale aux appelants, mais aussi une liberté maximale de choix comment le mettre en œuvre.

Ensuite, je vous recommande de fournir une implémentation aussi simple que possible et aussi propre que possible, sans aucun souci de performances. Pour moi, cela unordered_mapdevrait être votre premier choix, ou peut map- être si une sorte de commande de clés doit être exposée par l'interface.

Donc, faites-le d'abord fonctionner correctement et de façon minimale; ensuite, mettez-le à utiliser dans une application réelle; ce faisant, vous trouverez les problèmes que vous devez résoudre sur l'interface; ensuite, allez-y et abordez-les. La plupart des chances sont qu'à la suite de la modification de l'interface, vous devrez réécrire de grandes parties de l'implémentation, donc chaque fois que vous avez déjà investi dans la première itération de l'implémentation au-delà du strict minimum nécessaire pour l'obtenir juste à peine le travail est du temps perdu.

Ensuite, profilez-le et voyez ce qui doit être amélioré dans la mise en œuvre, sans modifier l'interface. Ou vous pouvez avoir vos propres idées sur la façon d'améliorer la mise en œuvre, avant même de profiler. C'est bien, mais il n'y a toujours aucune raison de travailler sur ces idées à un moment antérieur.

Vous dites que vous espérez faire mieux que map; on peut en dire deux choses:

a) vous ne le ferez probablement pas;

b) éviter à tout prix une optimisation prématurée.

En ce qui concerne l'implémentation, votre principal problème semble être l'allocation de mémoire, car vous semblez être préoccupé par la façon de structurer votre conception afin de contourner les problèmes que vous prévoyez que vous allez rencontrer en ce qui concerne l'allocation de mémoire. La meilleure façon de résoudre les problèmes d'allocation de mémoire en C ++ est d'implémenter une gestion d'allocation de mémoire appropriée, et non pas de tordre et de plier la conception autour d'eux. Vous devriez vous considérer chanceux d'utiliser C ++, ce qui vous permet de faire votre propre gestion d'allocation de mémoire, contrairement aux langages comme Java et C #, où vous êtes à peu près coincé avec ce que le runtime du langage a à offrir.

Il existe différentes façons de gérer la mémoire en C ++, et la possibilité de surcharger l' newopérateur peut être utile. Un allocateur de mémoire simpliste pour votre projet préallouerait un énorme tableau d'octets et l'utiliserait comme un tas. ( byte* heap.) Vous auriez un firstFreeByteindex, initialisé à zéro, qui indique le premier octet libre du tas. Lorsqu'une demande d' Noctets arrive, vous retournez l'adresse heap + firstFreeByteet vous ajoutez Nà firstFreeByte. Ainsi, l'allocation de mémoire devient si rapide et efficace qu'elle ne devient pratiquement plus un problème.

Bien sûr, préallouer toute votre mémoire peut ne pas être une bonne idée, vous devrez donc peut-être diviser votre tas en banques qui sont allouées à la demande et continuer à servir les demandes d'allocation de la banque la plus récente à tout moment donné.

Étant donné que vos données sont immuables, c'est une bonne solution. Il vous permet d'abandonner l'idée des objets de longueur variable et de faire en sorte que chacun Paircontienne un pointeur sur ses données comme il se doit, car l'allocation de mémoire supplémentaire pour les données ne coûte pratiquement rien.

Si vous voulez être en mesure de jeter des objets du tas, afin de pouvoir récupérer leur mémoire, alors les choses deviennent plus compliquées: vous devrez utiliser non pas des pointeurs, mais des pointeurs vers des pointeurs, de sorte que vous puissiez toujours déplacer des objets autour des tas de façon à récupérer l'espace des objets supprimés. Tout devient un peu plus lent en raison de l'indirection supplémentaire, mais tout est encore très rapide par rapport à l'utilisation de routines d'allocation de mémoire de bibliothèque d'exécution standard.

Mais tout cela est bien sûr inutile de s'inquiéter si vous ne construisez pas d'abord une version de travail simple et minimale de votre base de données, et que vous la mettez à utiliser dans une application réelle.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.