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 IList
classe abstraite . Il est hérité de trois classes
VectorList
- Tableau dynamique C - similaire à std :: vector, mais utiliserealloc
LinkList
- conçu pour les contrôles et la comparaison des performancesSkipList
- la classe qui sera finalement utilisée.
À l'avenir, je pourrais aussi faire de l' Red Black
arbre.
Chacun IList
contient zéro ou plusieurs pointeurs vers des paires, triés par clé.
S'il IList
est 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
IList
est recherchée (SkipList
,SkipList
ouLinkList
). - 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 IList
choses.
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_ptr
pointé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::Blob
dans 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 Pair
auquel je pense également est de supprimer la classe et de la remplacer par std::shared_ptr
et de "repousser" toutes les méthodes Pair::Blob
, mais cela ne m'aidera pas avec la Pair::Blob
classe 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
la source
std::map
oustd::unordered_map
? Pourquoi les valeurs (associées aux clés) en sont-ellesvoid*
? Vous auriez probablement besoin de les détruire à un moment donné; comment quand? Pourquoi n'utilisez-vous pas de modèles?IList::remove
ou 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 serastd::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.C string
et les données sont toujours un tamponvoid *
ouchar *
, donc vous pouvez passer un tableau char. Vous pouvez trouver similaire dansredis
oumemcached
. À un moment donné, je pourrais décider d'utiliserstd::string
ou d'un tableau de caractères fixe pour la clé, mais souligner que ce sera toujours la chaîne C.Réponses:
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_map
devrait être votre premier choix, ou peutmap
- ê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'
new
opé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 unfirstFreeByte
index, initialisé à zéro, qui indique le premier octet libre du tas. Lorsqu'une demande d'N
octets arrive, vous retournez l'adresseheap + firstFreeByte
et vous ajoutezN
à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
Pair
contienne 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.
la source