Ich entwickle einen Datenbankserver ähnlich wie Cassandra.
Die Entwicklung wurde in C begonnen, aber ohne Unterricht wurde es sehr kompliziert.
Momentan habe ich alles in C ++ 11 portiert, aber ich lerne immer noch "modernes" C ++ und habe Zweifel an vielen Dingen.
Die Datenbank funktioniert mit Schlüssel / Wert-Paaren. Jedes Paar hat weitere Informationen - wann wird es erstellt, auch wenn es abläuft (0, wenn es nicht abläuft). Jedes Paar ist unveränderlich.
Schlüssel ist C-Zeichenfolge, Wert ist ungültig *, aber zumindest für den Moment arbeite ich auch mit dem Wert als C-Zeichenfolge.
Es gibt abstrakte IList
Klassen. Es wird von drei Klassen geerbt
VectorList
- Dynamisches Array C - ähnlich wie std :: vector, verwendet jedochrealloc
LinkList
- für Überprüfungen und Leistungsvergleiche gemachtSkipList
- die Klasse, die endlich verwendet wird.
In Zukunft könnte ich auch Red Black
Baum machen.
Jedes IList
enthält null oder mehr Zeiger auf Paare, sortiert nach Schlüsseln.
Wenn es IList
zu lang wird, kann es in einer speziellen Datei auf der Festplatte gespeichert werden. Diese spezielle Datei ist eine Art read only list
.
Wenn Sie nach einem Schlüssel suchen müssen,
- zuerst im Speicher
IList
wird gesucht (SkipList
,SkipList
oderLinkList
). - Anschließend wird die Suche an die nach Datum sortierten Dateien gesendet
(neueste Datei zuerst, älteste Datei - letzte).
Alle diese Dateien werden im Speicher mmaped. - Wenn nichts gefunden wird, wird der Schlüssel nicht gefunden.
Ich habe keine Zweifel an der Umsetzung der IList
Dinge.
Was mich derzeit verwirrt, ist Folgendes:
Die Paare sind unterschiedlich groß, sie werden von zugeordnet new()
und sie haben std::shared_ptr
auf sie gezeigt.
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];
};
Die Mitgliedsvariable "buffer" ist die Variable mit unterschiedlicher Größe. Es speichert den Schlüssel + Wert.
Wenn der Schlüssel beispielsweise 10 Zeichen und der Wert weitere 10 Bytes beträgt, ist das gesamte Objekt sizeof(Pair::Blob) + 20
(der Puffer hat aufgrund von zwei nullterminierenden Bytes eine Anfangsgröße von 2).
Das gleiche Layout wird auch auf der Festplatte verwendet, sodass ich so etwas tun kann:
// 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);
Diese unterschiedliche Größe ist jedoch an vielen Stellen mit C ++ - Code ein Problem.
Zum Beispiel kann ich nicht verwenden std::make_shared()
. Dies ist wichtig für mich, denn wenn ich 1 Million Paare hätte, hätte ich 2 Millionen Zuweisungen.
Von der anderen Seite, wenn ich "Puffer" für dynamisches Array mache (z. B. neues Zeichen [123]), verliere ich mmap "Trick", ich muss zwei Dereferenzen durchführen, wenn ich den Schlüssel überprüfen möchte, und ich werde einen einzelnen Zeiger hinzufügen - 8 Bytes an die Klasse.
Ich habe auch versucht zu „pull“ alle Mitglieder von Pair::Blob
in Pair
, so Pair::Blob
nur der Puffer zu sein, aber wenn ich es getestet, es war ziemlich langsam, wahrscheinlich wegen des Kopierens der Objektdaten um.
Eine andere Änderung, an die ich auch denke, besteht darin, die Pair
Klasse zu entfernen und durch std::shared_ptr
alle Methoden zu ersetzen und sie zurückzuschieben. Pair::Blob
Dies hilft mir jedoch nicht bei Pair::Blob
Klassen mit variabler Größe .
Ich frage mich, wie ich das Objektdesign verbessern kann, um C ++ freundlicher zu sein.
Der vollständige Quellcode ist hier:
https://github.com/nmmmnu/HM3
quelle
std::map
oderstd::unordered_map
? Warum gibt es Werte (die Schlüsseln zugeordnet sind)void*
? Sie müssten sie wahrscheinlich irgendwann zerstören; wie wann? Warum verwenden Sie keine Vorlagen?IList::remove
oder wenn IList zerstört wird. Es braucht viel Zeit, aber ich werde es in einem separaten Thread tun. Es wird einfach sein, weil IList esstd::unique_ptr<IList>
sowieso sein wird. so kann ich es mit einer neuen Liste "wechseln" und das alte Objekt irgendwo aufbewahren, wo ich d-tor aufrufen kann.C string
und die Daten immer ein Puffer sindvoid *
oderchar *
Sie ein char-Array übergeben können. Sie finden ähnliche inredis
odermemcached
. Irgendwann könnte ich mich entscheiden, einstd::string
char-Array für den Schlüssel zu verwenden oder zu fixieren, aber unterstreiche, dass es immer noch eine C-Zeichenfolge ist.Antworten:
Der Ansatz, den ich empfehlen würde, besteht darin, sich auf die Benutzeroberfläche Ihres Schlüsselwertspeichers zu konzentrieren, um sie so sauber wie möglich und so uneingeschränkt wie möglich zu gestalten. Dies bedeutet, dass den Anrufern maximale Freiheit, aber auch maximale Auswahlfreiheit eingeräumt werden sollte wie man es umsetzt.
Dann würde ich empfehlen, dass Sie eine möglichst unkomplizierte und so saubere Implementierung wie möglich ohne Leistungsprobleme bereitstellen. Mir scheint, es
unordered_map
sollte Ihre erste Wahl sein, oder vielleicht,map
wenn eine Art Reihenfolge der Schlüssel von der Schnittstelle offengelegt werden muss.Lassen Sie es also zuerst sauber und minimal arbeiten. Setzen Sie es dann in einer realen Anwendung ein. Auf diese Weise finden Sie auf der Benutzeroberfläche die Probleme, die Sie beheben müssen. Dann sprechen Sie sie an. Die meisten Chancen stehen gut, dass Sie aufgrund einer Änderung der Benutzeroberfläche große Teile der Implementierung neu schreiben müssen, sodass Sie jedes Mal, wenn Sie bereits in die erste Iteration der Implementierung investiert haben, über die minimale Zeit hinaus investieren, die erforderlich ist, um sie gerecht zu machen Kaum Arbeit ist Zeitverschwendung.
Profilieren Sie es dann und sehen Sie, was in der Implementierung verbessert werden muss, ohne die Schnittstelle zu ändern. Oder Sie haben Ihre eigenen Ideen, wie Sie die Implementierung verbessern können, bevor Sie sich überhaupt profilieren. Das ist in Ordnung, aber es ist immer noch kein Grund, zu einem früheren Zeitpunkt an diesen Ideen zu arbeiten.
Sie sagen, Sie hoffen, es besser zu machen als
map
; Darüber können zwei Dinge gesagt werden:a) Sie werden es wahrscheinlich nicht tun;
b) Vermeiden Sie um jeden Preis eine vorzeitige Optimierung.
In Bezug auf die Implementierung scheint Ihr Hauptproblem die Speicherzuweisung zu sein, da Sie sich anscheinend mit der Strukturierung Ihres Designs befassen, um Probleme zu umgehen, von denen Sie erwarten, dass sie in Bezug auf die Speicherzuweisung auftreten werden. Der beste Weg, um Bedenken hinsichtlich der Speicherzuweisung in C ++ auszuräumen, besteht darin, ein geeignetes Speicherzuweisungsmanagement zu implementieren, und nicht das Design um sie herum zu verdrehen und zu biegen. Sie sollten sich glücklich schätzen, dass Sie C ++ verwenden, mit dem Sie Ihre eigene Speicherzuweisungsverwaltung durchführen können, im Gegensatz zu Sprachen wie Java und C #, bei denen Sie ziemlich genau wissen, was die Sprachlaufzeit zu bieten hat.
Es gibt verschiedene Möglichkeiten zur Speicherverwaltung in C ++, und die Möglichkeit, den
new
Bediener zu überlasten , kann nützlich sein. Ein vereinfachter Speicherzuweiser für Ihr Projekt würde eine große Anzahl von Bytes vorab zuweisen und als Heap verwenden. (byte* heap
.) Sie hätten einenfirstFreeByte
Index, der auf Null initialisiert ist und das erste freie Byte im Heap angibt. Wenn eine Anforderung fürN
Bytes eingeht, geben Sie die Adresse zurückheap + firstFreeByte
und fügenN
sie hinzufirstFreeByte
. Die Speicherzuweisung wird so schnell und effizient, dass sie praktisch kein Problem mehr darstellt.Natürlich ist es möglicherweise keine gute Idee, Ihren gesamten Speicher vorab zuzuweisen. Daher müssen Sie Ihren Haufen möglicherweise in Banken aufteilen, die bei Bedarf zugewiesen werden, und weiterhin Zuweisungsanfragen von der jeweils neuesten Bank bearbeiten.
Da Ihre Daten unveränderlich sind, ist dies eine gute Lösung. Sie können die Idee von Objekten variabler Länge aufgeben und jedes Objekt
Pair
einen Zeiger auf seine Daten enthalten lassen, da die zusätzliche Speicherzuweisung für die Daten praktisch nichts kostet.Wenn Sie Objekte aus dem Heap verwerfen möchten, um ihren Speicher zurückzugewinnen, werden die Dinge komplizierter: Sie müssen keine Zeiger verwenden, sondern Zeiger auf Zeiger, damit Sie Objekte jederzeit verschieben können in den Haufen herum, um den Platz gelöschter Objekte zurückzugewinnen. Durch die zusätzliche Indirektion wird alles etwas langsamer, aber im Vergleich zur Verwendung von Standardroutinen für die Speicherzuweisung zur Laufzeitbibliothek ist alles immer noch blitzschnell.
Aber all dies ist natürlich wirklich nutzlos, wenn Sie nicht zuerst eine einfache, minimalistische und funktionierende Version Ihrer Datenbank erstellen und diese in einer realen Anwendung verwenden.
quelle