I am developing a database server similar to Cassandra.
Development were started in C, but things became very complicated without classes.
Currently I ported everything in C++11, but I am still learning "modern" C++ and have doubts about lot of things.
Database will work with Key / Value pairs. Every pair have some more information - when is created also when it will expire (0 if not expires). Each Pair is immutable.
Key is C string, Value is void *, but at least for the moment I am operating with the value as C string as well.
There are abstract IList
class. It is inherited from three classes
VectorList
- C dynamic array - similar to std::vector, but usesrealloc
LinkList
- made for checks and performance comparisonSkipList
- the class that finally will be used.
In the future I might do Red Black
tree as well.
Each IList
contains zero or more pointers to pairs, sorted by key.
If IList
became too long, it can be saved on the disk in a special file. This special file is kind of read only list
.
If you need to search for a key,
- first in memory
IList
is searched (SkipList
,SkipList
orLinkList
). - Then search is send to the files sorted by date
(newest file first, oldest file - last).
All these files are mmap-ed in memory. - If nothing found, then key is not found.
I have no doubts about the implementation of the IList
things.
What is currently puzzling me is following:
The pairs are with different size, they are allocated by new()
and they have std::shared_ptr
pointed to them.
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];
};
"buffer" member variable is the one with different size. It stores the key + value.
E.g. if key is 10 characters, and value is another 10 bytes, the whole object will be sizeof(Pair::Blob) + 20
(buffer have initial size of 2, because of two null terminating bytes)
This same layout is used on the disk as well, so I can do something like this:
// 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);
However this different size is a problem on lots of places with C++ code.
For example I can not use std::make_shared()
. This is important for me, because if I have 1M Pairs, I would have 2M allocations.
From the other side, If I do "buffer" to dynamic array (e.g. new char[123]), I will lose mmap "trick", I will have do two dereferences if I want to check the key and I will add single pointer - 8 bytes to the class.
I also tried to "pull" all members from Pair::Blob
into Pair
, so Pair::Blob
to be just the buffer, but when I tested it, it was quite slow, probably because of copying the object data around.
Another change I am also thinking is to remove Pair
class and replace it with std::shared_ptr
and to "push" all methods back to Pair::Blob
, but this will not help me with variable size Pair::Blob
class.
I am wondering how I can improve on the object design in order to be more C++ friendly.
The full source code is here:
https://github.com/nmmmnu/HM3
std::map
orstd::unordered_map
? Why are values (associated to keys) somevoid*
? You probably would need to destroy them at some point; how & when? Why don't you use templates?IList::remove
or when IList is destructed. It takes lot of time, but I am going to do in separate thread. It will be easy because IList will bestd::unique_ptr<IList>
anyway. so I will be able to "switch" it with new list and keep the old object somewhere where I can call d-tor.C string
and data is always some buffervoid *
orchar *
, so you can pass char array. You can find similar inredis
ormemcached
. At some point I could decide to usestd::string
or fixed char array for key, but underline it will be still C string.