Leopold-Franzens-Universität Innsbruck Institut für Informatik Datenbanken und Informationssysteme Evaluierung von sortierten hauptspeicherbasierten Indexstrukturen Bachelor-Arbeit Christof Rinner betreut von Dipl.-Ing. Robert Binna Innsbruck, 25. Juni 2012 Zusammenfassung Steigende Arbeitsspeicherkapazitäten ermöglichen es Datenbanksystemen, welche zuvor ausschließlich auf Festplatten zur Datenverwaltung gesetzt haben, den Großteil ihrer Bestandteile in den schnelleren Arbeitsspeicher zu verlagern. Um eine schnelle Anfrageverarbeitung zu ermöglichen, werden in nahezu allen Systemen spezielle Indexstrukturen eingesetzt, welche einen geordneten Zugriff beschleunigen. Mit dem Schwenk vom Sekundärspeicher zum Hauptspeicher als primäres Speichermedium haben sich auch die Anforderungen an diese Indexstrukturen in Bezug auf verringerte Latenzzeiten und Speicherhierarchien grundlegend geändert. Daher ist das Ziel dieser Arbeit die theoretische und experimentelle Evaluierung von Indexstrukturen, welche einen sortierten Zugriff innerhalb des Hauptspeichers unterstützen. Zusätzlich zu den bekannten Indexstrukturen wird eine neue Variante der Komprimierung beim B-Baum mit Differenzwerten vorgestellt. Abstract Increasing main memory capacities allow for major parts of database systems to be moved from hard disc into the much faster main memory. To support a fast and efficient query execution almost all systems use index structures to speed up point- as well as range queries. With the shift from disc based to main memory based database systems also the demands for order-preserving index structures shifted. Besides the reduced latencies also the different memory hierarchies have to be considered. Therefore the goal of this thesis is the theoretical and experimental evaluation of main memory index structures which feature ordered access pattern. Beside known datastructures a new approach for compressing b-trees with difference values is presented. Inhaltsverzeichnis 1 Einleitung 1 2 Binäre Suchbäume 2 2.1 2.2 2.3 Natürlicher binärer Suchbaum . . . . . . . . . . . . . . . . 2 2.1.1 Such-Operation . . . . . . . . . . . . . . . . . . . . 3 2.1.2 Einfüge-Operation . . . . . . . . . . . . . . . . . . 3 2.1.3 Lösch-Operation . . . . . . . . . . . . . . . . . . . 4 2.1.4 Durchlaufen des Suchbaums . . . . . . . . . . . . . 5 Selbstbalancierende Bäume . . . . . . . . . . . . . . . . . 6 2.2.1 Rot-Schwarz-Baum . . . . . . . . . . . . . . . . . . 6 2.2.2 AVL-Baum . . . . . . . . . . . . . . . . . . . . . . 9 2.2.3 Scapegoat-Baum . . . . . . . . . . . . . . . . . . . 10 Verwandte Arbeiten . . . . . . . . . . . . . . . . . . . . . 11 2.3.1 AA-Baum und LLRB . . . . . . . . . . . . . . . . 11 2.3.2 T-Baum . . . . . . . . . . . . . . . . . . . . . . . . 11 3 Mehrwegbäume 3.1 B-Baum . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 3.1.1 3.2 3.3 13 Operationen . . . . . . . . . . . . . . . . . . . . . . 14 Komprimierung von IDs und Schlüsseln . . . . . . . . . . 15 3.2.1 Statische Komprimierung im B-Baum . . . . . . . 15 3.2.2 Dynamische Komprimierung mit Differenzen . . . 15 Verwandte Arbeiten . . . . . . . . . . . . . . . . . . . . . 17 II INHALTSVERZEICHNIS 3.3.1 CSB+ -Baum . . . . . . . . . . . . . . . . . . . . . 17 4 Evaluierung der Bäume 19 4.1 Speicherverbrauch . . . . . . . . . . . . . . . . . . . . . . 19 4.2 Such-Operation . . . . . . . . . . . . . . . . . . . . . . . . 20 4.3 Einfügen und Löschen . . . . . . . . . . . . . . . . . . . . 20 5 Benchmarks 21 5.1 Testsystem . . . . . . . . . . . . . . . . . . . . . . . . . . 22 5.2 Untersuchte Datenstrukturen . . . . . . . . . . . . . . . . 22 5.3 5.2.1 Natürlicher binärer Suchbaum . . . . . . . . . . . 22 5.2.2 Rot-Schwarz-Bäume . . . . . . . . . . . . . . . . . 22 5.2.3 B-Baum . . . . . . . . . . . . . . . . . . . . . . . . 26 Ausgwählte Resultate . . . . . . . . . . . . . . . . . . . . 27 6 Zusammenfassung und Ausblick 30 Appendix 31 A.1 Implementierungsdetails . . . . . . . . . . . . . . . . . . . 31 A.1.1 Komprimierung der Farbinformation des Rot-SchwarzBaums . . . . . . . . . . . . . . . . . . . . . . . . . 31 A.2 Speicherverwaltung . . . . . . . . . . . . . . . . . . . . . . 31 A.2.1 Anlegen des Speichers und von Datenstrukturen . 32 Literaturverzeichnis 36 III Kapitel 1 Einleitung An Datenstrukturen im Hauptspeicher werden andere Anforderungen gestellt, als an solche die für die Festplatte ausgelegt sind. B-Bäume sind bei konventionellen Datenbanken Standard, da diese besonders geeignet für Festplatten mit ihren hohen Latenzzeiten beim Zugriff sind. Diese Latzenzzeit fällt bei hauptspeicherbasierten Indexstrukturen weg, aber auch hier gibt es die Gegebenheiten zu beachten. Es ist anzunehmen, dass auch hier der Speicherzugriff der limitierende Faktor ist. Deshalb ist bei diesen Datenstrukturen die Cache-Optimierung ein wichtiger Punkt. Durch die Verfügbarkeit immer größerer Arbeitsspeicher werden Hauptspeicherdatenbanken immer verbreiteter. An die verwendeten Datenstrukturen werden so neue Anforderungen gestellt. Beispielsweise soll es möglich sein Daten persistent zu halten. Das setzt voraus, dass keine absoluten Adressen verwendet werden. Um dies zu erreichen wird eine eigene Speicherverwaltung entwickelt, welche die verwendeten relativen Adressen auflöst. Um zu einem objektiven Ergebnis zu kommen, werden die Datenstrukturen nicht nur einer theoretischen Evaluierung unterzogen, sondern auch experimentell auf ihr Laufzeitverhalten hin untersucht. Die Arbeit gliedert sich in einen theoretischen und praktischen Bereich. Die Kapitel 2 und 3 gehen auf die verbreiteten Datenstrukturen ein. Im Anschluss wird die Evaluierung der Datenstrukturen vorgenommen. Das Kapitel 5 umfasst die Benchmarks. Abgeschlossen wird die Arbeit von einer Zusammenfassung und einem Ausblick auf die weitere Entwicklung in diesem Themenbereich. 1 Kapitel 2 Binäre Suchbäume In diesem Kapitel werden Suchbäume mit zwei Nachfolgern vorgestellt. Zuerst wird der natürliche Binärbaum mit seinen Operationen erklärt. Weiters wird auf drei verschiedene selbstbalancierende Binärbäume eingegangen. Bei den behandelten Datenstrukturen handelt es sich um den Rot-Schwarz-Baum, den AVL-Baum und den Scapegoat-Baum. 2.1 Natürlicher binärer Suchbaum Der hier vorgestellte Baum bietet im Gegensatz zu allen anderen in dieser Arbeit vorgestellten Datenstrukturen keine Maßnahmen zur Balancierung. Daraus resultiert eine schlechte worst-case-Laufzeit. Die Operationen der in Unterabschnitt 2.2 erläuterten balancierten Bäume bauen jedoch auf denen des natürlichen Baums auf. Deshalb werden diese im Folgenden vorgestellt. Die Abbildung 2.1 zeigt einen Binärbaum mit vier Schlüsseln. 2 KAPITEL 2. BINÄRE SUCHBÄUME 20 17 25 18 nil nil nil nil nil Abbildung 2.1: Binärer Suchbaum. 2.1.1 Such-Operation Aufgrund der vorhandenen Ordnung der Schlüssel sind jeweils im linken Teilbaum Schlüssel kleiner bzw. im rechten Teilbaum größer als im Vaterknoten angeordnet. Daher muss man nur den Teilbaum untersuchen in welchem der gesuchte Schlüssel liegen müsste. Liegt ein perfekt balancierter Baum vor (der linke und rechte Teilbaum jedes Knoten besitzt die exakt selbe Höhe), teilt man die zu betrachtende Schlüsselmenge bei jedem Schritt. Dadurch ergibt sich eine Komplexität von O(log2 (n)) bei n Knoten. Im Vergleich dazu hat die Suche in unsortierten Listen eine Komplexität von O(n), weil um sicherzugehen ob ein Element vorhanden ist, möglicherweise alle Schlüssel betrachtet werden müssen. Beim Suchen eines Schlüssels beginnt man bei der Wurzel. Ist die Wurzel ein Blatt (leerer Baum) ist die Suche bereits beendet und der Schlüssel ist nicht im Baum enthalten. Ansonsten vergleicht man den gesuchten Schlüssel mit dem Schlüssel des Knotens. Sollte der Wert kleiner als dieser sein durchsucht man den linken Teilbaum, sonst den rechten. Dieser Vorgang wird wiederholt bis entweder der gesuchte Schlüssel gefunden wird oder man zu einem Blatt gelangt. Das Listing 2.1 zeigt diesen Vorgang in Pseudo-Code. 2.1.2 Einfüge-Operation Beim Einfügen eines Schlüssels wird zunächst die Suche verwendet, um das Blatt in dem der Schlüssel eingefügt werden soll zu bestimmen. Findet man jedoch den Schlüssel wird die Operation abgebrochen. Ansonsten wird das Blatt durch den einzufügenden Schlüssel ersetzt. Die beiden Kinder sind dann jeweils ein Blatt. 3 KAPITEL 2. BINÄRE SUCHBÄUME Die Abbildung 2.2 zeigt die Einfügeoperation des Knotens 18. Links wird das Blatt, welches im ersten Schritt als Einfügepunkt bestimmt wurde, rot gekennzeichnet. 2.1.3 Lösch-Operation Zum Löschen wird wiederum der zu löschende Schlüssel gesucht. Wird dieser nicht gefunden bricht der Vorgang ab. Ansonsten können drei Fälle auftreten: Knoten hat keinen Sohn Knoten kann einfach entfernt werden. Knoten hat einen Sohn Lösche den Knoten und ersetze ihn durch seinen Sohn. Knoten hat zwei Nachfolger Suche den kleinsten Schlüssel im rechten Teilbaum des zu löschenden Knoten. Dieser Knoten entspricht dem am weitesten links stehenden Knoten im Teilbaum. Dieser ersetzt den zu löschenden. Diese Vorgehensweise wird in Abbildung 2.3 anhand des Löschens von Schlüssel 17 gezeigt. Listing 2.1: Pseudo-Code für die rekursive Suche in einem Binärbaum. f i n d ( node , key ) { i f ( node == NIL | | key == node . key ) return node ; i f ( key < node . key ) return f i n d ( node . l e f t , key ) e l s e return f i n d ( node . r i g h t , key ) ; } 4 KAPITEL 2. BINÄRE SUCHBÄUME 20 20 17 nil 25 nil nil 17 nil 25 18 nil nil nil nil nil Abbildung 2.2: Der rechte Baum zeigt den linken Baum nach dem Einfügen des Schlüssels 18. 20 17 14 20 25 14 18 25 18 Abbildung 2.3: Löschen eines Knoten mit zwei Kindern. 2.1.4 Durchlaufen des Suchbaums Will man die Elemente eines Baumes durchlaufen, gibt es verschiedene Suchstrategien. Wenn die Schlüssel in aufsteigender/absteigender Reihenfolge durchlaufen werden sollen, kommt die symmetrische Reihenfolge (auch als Inorder bezeichnet) zur Anwendung. Diese kann durch den rekursiven Algorithmus in Listing 2.2 implementiert werden: 5 KAPITEL 2. BINÄRE SUCHBÄUME Listing 2.2: Pseudo-Code zum Durchlaufen eines Baumes in symmetrischer Reihenfolge. s y m e t r i c t r a v e r s a l ( node ) { i f ( node == NIL ) return ; s y m e t r i c t r a v e r s a l ( node . l e f t ) ; p r i n t ( node . key ) ; s y m e t r i c t r a v e r s a l ( node . r i g h t ) ; } Der Parameter beim Aufruf der Funktion entspricht dem Startpunkt des Durchlaufs. Für eine Durchlauf des gesamten Baumes wählt man den Wurzelknoten. 2.2 Selbstbalancierende Bäume Da viele Operationen des binären Suchbaums auf der Such-Operation aufbauen ist eine möglichst hohe Geschwindigkeit dieser Operation anzustreben. Grundsätzlich hat diese eine Komplexität von O(h), wobei h für die Höhe des Baums steht. Da binäre Suchbäume keine eindeutige Darstellung haben, können die Reihenfolge des Einfügens und Löschens Einfluss auf den Aufbau des Baums haben. Abbildung 2.4 zeigt zwei Bäume, die aus denselben Schlüsseln bestehen, jedoch wurden diese in verschiedener Reihenfolge eingefügt. Dies hat zur Folge, dass sich die Höhe der Bäume unterscheiden kann und so folglich auch die Laufzeit der Such-Operation. Im Falle der linken Abbildung spricht man von einem degenerierten Baum. Die Komplexität der Suche in diesem Fall entspricht derer der unsortierten Liste: O(n). In diesem Abschnitt werden Bäume behandelt, welche unabhängig von der Reihenfolge der Update-Operationen, also dem Einfügen und Löschen, nicht degenerieren können. 2.2.1 Rot-Schwarz-Baum Rot-Schwarz-Bäume wurden von Rudolf Bayer erfunden, der sie als “symmetric binary B-Trees” bezeichnete. [Bay72] Der nunmehr gebräuchliche Name wurde von Leo J. Guibas und Robert Sedgewick eingeführt. [GS78] Der Rot-Schwarz-Baum unterstützt alle Operationen des Suchbaums. Die Implementierung der Einfüge- und Lösch-Operation baut auf den 6 KAPITEL 2. BINÄRE SUCHBÄUME 2 4 3 2 4 5 3 7 5 7 Abbildung 2.4: Einfügereihenfolge 2, 3, 4, 5, 7 bzw.4, 2, 5, 7, 3 Operationen des natürlichen Binärbaums auf, jedoch sorgen diese zusätzlich dafür, dass der Baum balanciert bleibt. Die maximale Höhe ist 2·log2 (n + 1). Das bedeutet, dass ein Pfad zu einem Blatt maximal doppelt so lang, als alle anderen sein kann. Sowohl der average als auch der worst-case der Suche beläuft sich somit auf O(log2 (n)). Jeder Knoten eines Rot-Schwarz-Baums hat ein zusätzliches Attribut, welches die Farbe des Knotens (entweder rot oder schwarz) speichert. Ein Rot-Schwarz-Baum muss die in [CLRS09] definierten Bedingungen erfüllen: 1. Jeder Knoten ist entweder rot oder schwarz. 2. Der Wurzelknoten ist schwarz. 3. Jedes Blatt ist schwarz. 4. Beide Kinder eines roten Knotens sind schwarz. 5. Alle einfachen Pfade von einem Knoten zu den Blättern haben gleich viele schwarze Knoten. Die Abbildung 2.5 zeigt einen Baum, der alle genannten Bedingungen erfüllt. Diese können durch Update-Operationen verletzt werden. Darum müssen diese nach einer solchen Operation überprüft werden und gegebenenfalls durch Rotationen und Änderungen der Farbe wiederhergestellt werden. 7 KAPITEL 2. BINÄRE SUCHBÄUME 20 17 8 25 19 22 28 nil nil nil nil nil nil nil nil Abbildung 2.5: Ein Rot-Schwarz-Baum, der alle Eigenschaften erfüllt. y x y x 3 1 2 1 2 3 Abbildung 2.6: Ausgleich der Höhendifferenz im linken Baum mittels einer Rechtsrotation. Solch eine Rotation wird in Abbildung 2.6 gezeigt. Dabei handelt es sich um eine Rechtsrotation, Linksrotationen verlaufen äquivalent. Bei 1,2 und 3 handelt es sich um beliebige Teilbäume. Eine Rotation hat eine Komplexität von O(1), da nur Pointer geändert werden müssen. Die Einfüge-Operation Das Einfügen eines neuen Knotens basiert auf dem Einfügen des binären Suchbaums. Der neue Knoten wird rot gefärbt. Die Kinder des neuen Knotens sind beide schwarz. Danach wird eine Operation aufgerufen, welche überprüft, ob die Bedingungen erfüllt werden und wenn nötig Rotationen und Farbänderungen vornimmt. Bei einem Einfügevorgang kann es zu maximal 2 Rotationen kommen. [CLRS09, ] Durch das Einfügen eines neuen Knotens können die Bedingungen 2 und 4 verletzt werden. Die Bedingung 2 wird nur verletzt wenn der neue 8 KAPITEL 2. BINÄRE SUCHBÄUME Knoten die Wurzel ist. Diese ist dann wider den Bedingungen rot. Bedingung 4 wird verletzt, wenn der Vater des neuen Knotens rot ist. Die Lösch-Operation Wie schon beim Einfügen wird auch beim Löschen eines Knotens nach den Regeln des natürlichen Binärbaums vorgegangen und anschließend die Farbeigenschaften überprüft. Nach dem Löschen können die Eigenschaften 2, 4 und 5 verletzt sein. Zum Auflösen der Konflikte werden höchstens 3 Rotationen benötigt. Es kann neben den Rotationen auch nötig sein Schlüssel zu verschieben. (maximal O(log2 (n))-mal) Die Gesamtkomplexiät beträgt ebenfalls O(log2 (n)).[CLRS09] Komprimierung der Farbinformation Da man mit einem 64-BitPointer 264 Byte addressieren kann, würden im theoretischen Fall, dass der gesamte Arbeitsspeicher mit Knoten von 24 Byte (2 Nachfolger und der Schlüssel mit jeweils 8 Byte) befüllt ist, Bits ungenützt bleiben. Diese können zum mitspeichern der Balanceinformationen genutzt werden. Das heißt, es wird kein zusätzlicher Speicherplatz benötigt und durch die kleinere Knotengröße wird die Anzahl der im Cache Platz findenden Knoten erhöht. Das Decodieren der Variable aus dem Pointer kann sich aber auch negativ auf die Performance auswirken. Beispielsweise liefert ie Implementierung des Rot-Schwarz-Baums in der boost-Bibliothek1 einen Schalter mit, der diese Möglichkeit bietet. 2.2.2 AVL-Baum AVL-Bäume zählen zu den höhenbalancierten Bäumen. Ein Suchbaum gilt als AVL-Baum, wenn sich die Höhe der Teilbäume eines jeden Knotens höchstens um eins unterscheidet. Jeder Knoten hat ein Attribut, welches die Höhendifferenz der beiden Teilbäume speichert. Dieses Attribut kann die Werte 0, 1 und -1 annehmen. Null symbolisiert, dass die Teilbäume gleich hoch sind. Eins bedeutet, dass der rechte Teilbaum um eine Ebene höher ist, minus eins um eine niedriger. [OW02] Bei einer Operation welche die Struktur des Baumes ändert, also entweder Einfügen oder Löschen, führt man zunächst die eigentliche Aktion aus. Anschließend wird überprüft, ob die Balanciertheit der Teilbäume erfüllt ist. Eine Verletzung der Bedingungen liegt vor, wenn der Höhenunterschied der Teilbäume 1 bzw. -1 überschreitet. Es werden höchstens O(log2 (n)) Rotationen benötigt, um die Balance wiederherzustellen. 1 http://www.boost.org 9 KAPITEL 2. BINÄRE SUCHBÄUME 2.2.3 Scapegoat-Baum Scapegoat-Bäume unterscheiden sich von den anderen selbstbalancierenden binären Suchbäumen dahingehend, dass die Knoten keine zusätzlichen Informationen beinhalten, um die Balanciertheit zu gewährleisten. RotSchwarz-Bäume benötigen ein zusätzliches Bit, um die Farbe des Knoten zu speichern und AVL-Bäume 2 Bits für die Höhendifferenz. Die einzigen zusätzlichen Daten die benötigt werden sind zwei Werte. Diese Werte sind zum einen die Anzahl der Knoten des Baums und zum anderen die maximale Anzahl der Knoten seit der Baum zum letzten mal komplett neu aufgebaut wurde. Diese Werte werden im Weiteren als size und max size bezeichnet.[GR93] Die Erfinder des Scapegoat-Baums Igal Galperin und Ronald L. Rivest verwenden zur Beschreibung der Balance die Begriffe α-weight-balanced und α-height-balanced. Bei dieser Definition ist α ein Wert im Intervall [0.5,1], der die Strenge der Balancierung angibt. Ein Knoten ist α-weight-balanced, wenn die folgenden zwei Bedingungen zutreffen: • size(node.lef t) <= α · size(node) • size(node.right) <= α · size(node) und height-balanced wenn für einen Baum T gilt: height(T) < hα(size(T )) , wobei hα (n) sich folgendermaßen berechnet: hα (n) = blog1/α nc Operationen Die Such-Operation des Scapegoat-Baums entspricht der des binären Suchbaums, jedoch unterscheiden sich die Einfüge- und Lösch-Operation. Beim Einfügen wird size um eins erhöht. Außerdem wird max size auf das Maximum von max und max size gesetzt. Sollten diese neuen Werte zu einer Verletzung der Bedingungen führen, wird der neue Knoten als ein tiefer Knoten bezeichnet. In diesem Fall klettert man den Baum hinauf zur Wurzel und überprüft für jeden Knoten ob er α-weight-balanced ist. Ist dies nicht der Fall wird der Knoten als scapegoat (Sündenbock) bezeichnet. Dann wird der Baum unter dem Sündenbock neu aufgebaut, um die Balance wiederherzustellen. Ist die Wurzel der Sündenbock, muss der komplette Baum neu aufgebaut werden, weshalb die Komplexität in diesem Fall O(n) ist. Dieser Fall tritt jedoch sehr selten auf, dadurch wird die amortisierte Laufzeit mit O(log2 (n)) angegeben. 10 KAPITEL 2. BINÄRE SUCHBÄUME Will man einen Knoten löschen, geht man wie beim binären Suchbaum vor und verringert zusätzlich size um eins. Ist size kleiner als α·max size muss der gesamte Baum neu aufgebaut werden. 2.3 2.3.1 Verwandte Arbeiten AA-Baum und LLRB Bei diesen beiden Baumtypen handelt es sich um Varianten des RotSchwarz-Baumes. Der AA-Baum wurde von Arne Andersson mit dem Ziel einer möglichst einfachen Impelementierbarkeit erfunden. Beim Rot-Schwarz-Baum sind bei der Wiederherstellung der Balance 7 verschiedene Fälle zu betrachten. Hingegen wurde dies beim AA-Baum auf 2 Fälle reduziert.[And93] Erreicht wird dies, indem zu den üblichen Bedingungen des Rot-SchwarzBaums ergänzt wird, dass ein roter Knoten nur das rechte Kind des Vaters sein darf. Wird diese Bedingung verletzt muss eine Rotation durchgeführt werden. Auch beim LLRB (Left-leaning Red-Black Trees) von Sedgewick [Sed08] wird eine neue Bedingung eingeführt. Hier darf ein roter Knoten nur das linke Kind sein. Außerdem werden die Operationen mit rekursiven Funktionen, welche leicht verständlich sein sollen, beschrieben. Geschwindigkeit war bei der Entwicklung nicht so sehr im Fokus wie Einfachheit. Trotzdem haben die Bäume die gleichen Laufzeiten wie der Rot-Schwarz-Baum. 2.3.2 T-Baum Eine andere binäre Datenstruktur, welche in vielen Hauptspeicherdatenbanken Verwendung findet, ist der T-Baum. Ein T-Baum Knoten, wie in Abbildung 2.7, besteht aus maximal 2 Nachfolgern und einer gewissen Anzahl von Schlüsseln, welche direkt im Knoten gespeichert werden. Diese werden wie beim B-Baum (siehe Abschnitt 3.1) sortiert gespeichert. Der T-Baum arbeitet bei der Balancierung mit denselben Verfahren wie AVL-Bäume. Jedoch besteht ein Knoten aus mehreren Schlüsseln und somit sind Rotationen seltener notwendig. Um einen Schlüssel einzufügen wird zunächst der passende Knoten gesucht. Dabei handelt es sich um einen Knoten in dem der neue Schlüssel zwischen dem kleinsten und größten Schlüssel liegt. Gibt es keinen solchen Knoten, wird der Schlüssel in ein Blatt eingefügt. 11 KAPITEL 2. BINÄRE SUCHBÄUME Abbildung 2.7: Ein T-Baum Knoten.[LC86] Kommt es beim Einfügen zu einem Überlauf des Knotens, wird der Knoten eingefügt und dann der kleinste Schlüssel entfernt und im linken Teilbaum im rechtesten Blatt eingefügt. Ist auch dieses voll wird ein neues Blatt mit diesem Schlüssel erzeugt. Danach muss die Balance überprüft und möglicherweise wiederhergestellt werden. Beim Löschen kann es zu einem Unterlauf kommen. Um diesen auszugleichen wird der kleinste Schlüssel des größten Knotens (der Knoten mit dem größten minimalen Element) im rechten Teilbaum des gelöschten Knotens hierher verschoben. Handelt es sich um ein Blatt aus dem der Schlüssel gelöscht wird, kann dieser gelöscht werden. Sollte es nach dem Entfernen leer sein, oder bei einem Unterlauf mit seinem Bruder, wenn vorhanden, verschmolzen werden. Anschließend könnten Rotationen nötig werden. In dem Artikel [LC86] zeigten die Benchmarks, dass der T-Baum schneller als ein B-Baum ist. Seit der Veröffentlichung 1986 vergrößerte sich jedoch die Geschwindigkeits bei CPUs stärker als beim Arbeitsspeicher. So zeigen neuere Untersuchungen, etwa in [RR99], dass dies für moderne Hardware nicht mehr der Fall ist. 12 Kapitel 3 Mehrwegbäume 3.1 B-Baum B-Bäume sind eine 1972 von Rudolf Bayer und Edward M. McCreight vorgestellte Datenstruktur. Obwohl das relationale Datenbankmodell erst später entwickelt wurde, erwies sich diese Datenstruktur als ideal für den Datenbankzugriff auf Festplatten. Im Gegensatz zu binären Suchbäumen ist bei B-Bäumen der Verzweigungsgrad viel höher. Dadurch ergibt sich, dass der Baum dementsprechend niedriger ist. Dies bringt mit sich, dass auf dem Weg zu dem gesuchten Knoten weniger verschiedene Knoten angesteuert werden müssen. Dieser Umstand hat bei Festplatten den Vorteil, dass der teure Spurwechsel seltener erfolgen muss. 15 3 7 0 2 4 6 21 28 32 12 13 17 20 22 25 29 30 34 38 44 Abbildung 3.1: Ein B-Baum der Ordnung 2. Abbildung 3.2 zeigt einen validen B-Baum der Ordnung k=2. Durch k wird festgelegt wie viele Schlüssel minimal und maximal in einem Knoten sein müssen. Minimal muss jeder Knoten aus k und maximal aus 13 KAPITEL 3. MEHRWEGBÄUME 2·k Schlüsseln bestehen. Eine Ausnahme bildet hier nur der Wurzelknoten. Dieser darf das Minimum unterschreiten. In der Praxis werden für Implementierungen, welche für Festplatten ausgelegt sind, Verzweigungsgrade bis zu 2000 verwendet. Dies hängt von der Schlüsselgröße und der Speicherseitengröße ab. Genau wie beim binären Suchbaum gilt auch hier, dass jeweils die Schlüssel im linken Teilbaum kleiner bzw. im rechten größer sein müssen. Natürlich sind auch die Schlüssel in einem Knoten sortiert angeordnet. Weiters muss die Höhe aller Teilbäume eines Knoten gleich sein. Diese Bedingung stellt sicher, dass der Baum immer perfekt balanciert ist. 3.1.1 Operationen Such-Operation Die Suche funktioniert ähnlich der eines Binärbaums, jedoch kann es angebracht sein durch die hohe Anzahl an Schlüsseln in einem Knoten die Binärsuche zu verwenden. Einfüge-Operation Beim Einfügen eines neuen Schlüssels bestimmt man zunächst das Blatt, welches den Schlüssel aufnehmen muss. Hat nun das Blatt nicht seine maximale Größe erreicht (weniger als 2·k Schlüssel), kann der Schlüssel einfach an der richtigen Stelle im Knoten eingefügt werden. Ist nun aber der Knoten bereits voll, wird temporär ein Knoten mit 2·k+1 Schlüsseln angelegt. Dieser wird dann in 2 neue Knoten aufgespalten. Der linke Teilknoten beinhaltet die Schlüssel links vom Median und der rechte, die rechts vom Median. Der Median wird an den Vaterknoten weitergereicht und dort eingefügt. Ist auch dieser bereits voll, wiederholt sich der Teilungsschritt bis ein nicht maximal gefüllter Knoten gefunden wird oder man das Wurzelelement erreicht. Falls auch die Wurzel bereits das Schlüsselmaximum erreicht hat, legt man eine neue Wurzel an und verknüpt diese mit den beiden aus der ursprünglichen Wurzel entstandenen Knoten. Nur wenn es nötig ist die Wurzel aufzuspalten wächst der Baum in der Höhe. 15 20 22 27 22 15 20 27 29 Abbildung 3.2: Einfügen des Schlüssels 29 in den linken Baum der Ordnung 2. 14 KAPITEL 3. MEHRWEGBÄUME Lösch-Operation Nach dem Löschen eines Schlüssels, kann die Anforderungen nach minimal k Elementen verletzt werden. Liegt ein linkes oder rechtes Geschwister mit k Schlüsseln vor, können die Beiden verschmolzen werden. Ansonsten werden diese temporär verschmolzen und danach wieder aufgespalten (am Median). 3.2 Komprimierung von IDs und Schlüsseln Um den Verzweigungsgrad bei gleicher Knotengröße zu erhöhen, kann der Binärbaum um Komprimierungsmöglichkeiten erweitert werden. Im Folgenden werden verschiedene Ansätze dafür vorgestellt. 3.2.1 Statische Komprimierung im B-Baum Da die Datenstrukturen in einem eigenen Speicherbereich, indem die einzelnen Knoten mit IDs addressiert werden, können statt 64-Bit-IDs nur soviel Bits verwendet werden, wie nötig um jeden Knoten des Speicherpools zu addressieren. In [LA05] wird diese Vorgehensweise für verknüpfte Datenstrukturen vorgestellt. Dafür muss man im Vorhinein wissen wie viele Knoten maximal auftreten werden. Wird diese Zahl überschritten, muss der Baum neu aufgebaut werden. Reichen beispielsweise 3 Byte um alle Knoten zu addressieren kann der Verzweigungsgrad bei 256-Byte-Knoten von 15 auf 22 erhöht werden. Sind auch die Schlüssel nicht 8 Byte groß können auch diese verkleinert werden. 3.2.2 Dynamische Komprimierung mit Differenzen Durch die Komprimierung mit zur Compilezeit definierten Werten, wird die Flexibilität eingeschränkt. Deshalb wird eine dynamische Komprimierung mit Differenzwerten angedacht. Der jeweilige Werte berechnet sich aus der Summe der Schlüssel auf dem Pfad zum Schlüssel. Dadurch muss jeweils nur die Differenz zum Vater gespeichert werden. Die benötigte Byte-Anzahl für die Schlüssel wird in jedem Knoten gesondert festlegt. Der Wert für die IDs wird weiterhin über eine Konstante (zwischen 2 und 8) bei der Erstellung des Baumes definiert. Die Byteanzahl für einen Schlüssel kann 3-8 Bytes betragen. Bei Schlüssel mit nur 1 oder 2 Bytes könnte ein Überlauf dazu führen, dass eine Aufspaltung des Knotens nicht genügt. 15 KAPITEL 3. MEHRWEGBÄUME Die Belegung eines Knotens wird, wie in Abbildung 3.3 ersichtlich, im ersten Byte eines Knotens gespeichert und im zweiten die Byte-Anzahl für die Schlüssel des Knotens. size: 5 bytes: 3 22 27 32 40 50 ... Abbildung 3.3: Aufbau eines Knotens für die dynamische Komprimierung. Einfüge-Operation Beim Einfügen eines neuen Schlüssels muss nicht nur darauf Rücksicht genommen werden, ob der Schlüssel noch im Blatt Platz findet. Es muss auch überprüft werden ob der Schlüssel durch die bestehende Byte-Anzahl dargestellt werden kann. Sind diese beiden Voraussetzungen erfüllt, unterscheidet sich das Einfügen nicht vom unkomprimierten B-Baum. Ist dies nicht der Fall wird das Blatt aufgespalten. Bei dem linken Blatt kann nun überprüft werden, ob weniger Bytes ausreichen um das größte Element darzustellen. Beim rechten wird ebenfalls das größte Element überprüft und je nach Größe neu aufgebaut. Mehr Bytes sind nur nötig wenn der neue Schlüssel der größte im Knoten ist. Der Median wird an den Vater weitergereicht. Nach dem Einfügen des Medians müssen möglicherweise die Differenzn in der Ebene darunter angepasst werden. Lösch-Operation Beim Löschen wird der Schlüssel zunächst wie beim B-Baum üblich entfernt. Handelt es sich um den größten Schlüssel im Knoten (Schlüssel ganz rechts) wird zusätzlich überprüft ob die Komprimierung erhöht werden kann. Möglicherweise müssen auch wieder Differenzwerte angepasst werden. Erwartete Performance Durch die nötigen dynamischen Berechnungen kann sich die Performance nicht gegenüber einem Baum mit statischer Komprimierung, gleicher Pointer- und Schlüsselgröße, verbessern. Der Vorteil besteht darin, dass alle Schlüsselgrößen abgedeckt werden und trotzdem eine Komprimierung und Performancesteigerung gegenüber dem unkomprimierten Baum eintritt. 16 KAPITEL 3. MEHRWEGBÄUME 3.3 Verwandte Arbeiten Durch das große Interesse an Hauptspeicherdatenbanken sind Indexstrukturen für diese ein aktives Forschungsgebiet. Hier werden einige Alternativen in Form von Mehrwegbäume vorgestellt. 3.3.1 CSB+ -Baum Im Artikel ”Making B+ -trees cache conscious in main memory” wird eine neue Datenstruktur auf Basis von B+ -Bäumen vorgestellt.[RR00] Wie bei B+ -Bäumen üblich, werden die Daten in den Blättern gespeichert. Jedoch werden Veränderungen vorgenommen, um den Cache besser auszunützen. Dazu werden in einem Knoten nicht unbedingt k+1 Nachfolger bei k Schlüsseln gespeichert, sondern eine variable Anzahl. Die fehlenden Nachfolger lassen sich über Offsets zu den angegebenen berechnen. Dazu ist es erforderlich, dass alle Geschwister, welche durch einen Pointer in ihrer Position festgelegt werden, direkt aufeinanderfolgend im Speicher liegen. Dieser Verbund der hintereinander liegenden Geschwister wird als node-group bezeichnet. Besonders eignet sich diese Datenstruktur für Datenbestände, welche sich selten ändern, da Update-Operationen teuer sind. Der Grund dafür ist, dass bei Veränderungen der Schlüsselmenge in einem Knoten oft Kopiervorgänge nötig sind, um alle Geschwister wieder hintereinander im Speicher zu positionieren. Der Geschwindigkeitsvorteil bei der Suche ergibt sich aus dem größeren Anteil von Schlüsseln im Knoten und dadurch einem höheren Verzweigungsgrad bei gleicher Cache-Line-Size gegenüber einem B+ -Baum. CSB+ -Baum mit einem Pointer Wird nur ein Pointer für alle Kinder gespeichert, verweist dieser bei einem Knoten mit k Schlüsseln auf ein Speichersegment mit mindestens k+1 Knoten. Bei der Suche wird nun im Knoten das kleinste Element das größer als der gesuchte Knoten ist ausgewählt. Je nach dessen Position berechnet sich der Offset der zum Pointer dazugerechnet wird. Ist der Schlüssel kleiner als das minimale Element des Knotens, entspricht dieser Offset 0. CSB+ -Baum mit mehreren Pointer Sind mehrere Pointer pro Knoten vorgesehen, werden diese über die maximale Anzahl von Schlüsseln gleichverteilt. Die Adresse zu den Kindknoten berechnet sich dann über den Pointer links des kleinsten Schlüssels, größer dem Gesuchten und 17 KAPITEL 3. MEHRWEGBÄUME dem relativen Offset zu diesem. Wie bereits erwähnt sind Update-Operationen teuer. Durch die Verwendung von mehreren Pointern kann dies verbessert werden, weil die node-groups kleiner werden und so die zu kopierenden Bereiche kleiner sind. Die Verwendung von mehreren Pointern verschlechtert aber die Performance, weil der Schlüsselanteil im Knoten sinkt. Vollständiger CSB+ -Baum Bei einem vollständigen CSB+ -Baum werden die node-groups immer mit der maximal benötigten Größe angelegt. Das heißt für die maximale Anzahl von Geschwistern mit der maximalen Größe. Dadurch wird das Allozieren von node-groups nicht mehr beim Überlaufen von Knoten, sondern nur mehr beim Überlauf von node-groups notwendig. Performance Vollständige CSB+ -Bäume sind sowohl bei der Suche, als auch bei Update-Operationen den B+ -Bäumen überlegen. Sie benötigen jedoch sehr viel Speicher. CSB+ -Bäume mit einem Pointer sind sehr schnell bei der Suche, jedoch langsam bei Update-Operationen. Solche mit mehreren Pointern sind ausgewogener, und laut [RR00] den B+ Bäumen, bei Suche, Update-Operationen und auch beim Speicherverbrauch überlegen. 18 Kapitel 4 Evaluierung der Bäume Die amortisierten Komplexitäten der vorgestellten Bäume sind für den average-case dieselben. Die Ausnahme bildet hier nur der natürliche Binärbaum. Dieser wurde jedoch nur betrachtet, da die Operationen der anderen Datenstrukturen mit diesen eng verknüpft sind. Für die Operationen Suchen, Einfügen und Löschen ist die Komplexität jeweils O(log(n)). Jedoch unterscheiden sich die Komplexitäten im Detail. Diese Unterschiede werden in diesem Kapitel untersucht. Aufgrund der Ergebnisee wird entschieden, welche Datenstrukturen umgesetzt werden. 4.1 Speicherverbrauch Alle Datenstrukturen haben eine Speicherkomplexität von O(n). Werden die relativen Speicheradressen und der Schlüssel mit jeweils 8 Bytes angenommen, verbraucht ein Knoten eines Scapegoat-Baums insgesamt 24 Byte für die beiden Speicheradressen der Kinder und den Schlüssel. Der Rot-Schwarz-Baum benötigt zusätzlich ein Bit für die Farbinformation und der AVL-Baum 2 Bits für den Balancewert. Diese zusätzlichen Daten können aber, wie bereits beschrieben in anderen Variablen, welche nicht den gesamten Wertebereich nutzen, mitgespeichert werden. Beim B-Baum mit einem maximalen Belegungsgrad von 256 Schlüssel reicht ein Byte, um die derzeitige Belegung im Knoten zu speichern. Daraus ergibt sich eine Knotengröße von 1 + x · 8 + (x + 1) · 8 Byte für einen Knoten mit maximal x Schlüsseln und dementsprechend x+1 Kindern mit jeweils 8 Byte. Durch Komprimierung lassen sich auch hier Verbesserungen erzielen. Mögliche Vorgehensweisen wurden bereits im Abschnit 3.2 erläutert. 19 KAPITEL 4. EVALUIERUNG DER BÄUME 4.2 Such-Operation Die Performance der Such-Operation wird durch zwei Faktoren beeinflusst: der Anteil der Cachetreffer und die Höhe des Baums. Der AVLund B-Baum garantieren, dass der Baum absolut ausgeglichen bzw. höchstens um den Faktor 2 unterschiedlich hoch ist, während der ScapegoatBaum abhängig vom α-Wert balanciert ist. Dadurch ergibt sich beim AVL-Baum eine maximale Höhe von log2 (n), beim Rot-Schwarz-Baum 2 · log2 (n + 1) und beim Scapegoat-Baum log1/α (n) + 1. Beim B-Baum hängt die Höhe vom gewählten Verzweigungsgrad ab und beläuft sich auf logk ( n+1 2 ). Bei den Cache-Treffern könnte der B-Baum Vorteile haben, da mehrere Schlüssel in einem Knoten liegen und so, je nach Größe, eine gewisse Anzahl von Schlüsseln in derselben Cache-Line sind. 4.3 Einfügen und Löschen Der AVL-Baum und der Rot-Schwarz-Baum verfolgen hier ähnliche Verfahren: die Balanceunterschiede werden mittels Rotationen ausgeglichen. Der Unterschied besteht darin, dass auf verschiedene Weise festgelegt wird, wann und wo eine Rotation nötig ist. Beim AVL-Baum kommt es zu maximal log2 (n) Rotationen und beim Rot-Schwarz-Baum beim Einfügen zu maximal 2 und beim Löschen zu maximal 3 Rotationen. Der Scapegoat-Baum hingegen baut Teile des Baumes komplett neu auf. Die Implementierung ist zwar vergleichsweise einfach, jedoch für große Schlüsselmengen, wegen der größeren Höhe und des möglichen Neuaufbauens weniger geeignet. Beim B-Baum werden möglicherweise Knoten zusammengefügt bzw. aufgespalten. Dabei müssen abhängig von der Größe eines Knotens viele Daten kopiert werden. Um eine Häufung dieses Vorganges zu senken, sollte der Verzweigungsgrad möglichst hoch gehalten werden. Da der Speicherzugriff auch beim Arbeitsspeicher teuer ist, könnte Komprimierung als Möglichkeit zur Performancesteigerung dienen. 20 Kapitel 5 Benchmarks Um die Performance der verschiedenen Datenstrukturen zu vergleichen, wurde eine einfache Benchmarksuite erstellt. Mit dieser können die jeweils gewünschten Datenstrukturen anhand einer angegeben Operation und eines Testsamples getestet werden. Außerdem ist es möglich die Datenstrukturen initial mit Schlüsseln zu füllen. Vor der Messung wird der Cache angewärmt. Anschließend werden mehrere Durchläufe ausgeführt und der Durchschnitt berechnet. Abbildung 5.1: Beispielhafte Gnuplot-Ausgabe eines Benchmarks. 21 KAPITEL 5. BENCHMARKS Abbildung 5.1 zeigt die Ausgabe eines Benchmarkdurchlaufs. Auf der x-Achse wird die Anzahl der verarbeiteten Knoten und auf der y-Achse die verstrichene Zeit in Millisekunden aufgetragen. 5.1 Testsystem Die Benchmarks wurden auf einem Intel Core i5 Quad-Core System mit 2,66GHz und 6GB Ram durchgeführt. Die CPU verfügt über 64KB L1, 256KB L2 Cache pro Core und 8MB geteilt benützten L3 Cache. Die Cache Line Size beträgt 64 Byte. 5.2 5.2.1 Untersuchte Datenstrukturen Natürlicher binärer Suchbaum Die Implementierung orientiert sich am Pseudo-Code der in [CLRS09] angegeben wird. Es wurden aber Änderungen vorgenommen, um relative Adressen für die Knotenaddressierung zu verwenden. Bei den weiteren Benchmarks wird auf die Darstellung des natürlichen Binärsuchbaums verzichtet. Die Abbildung 5.2 zeigt das Einfügen von 10000 Schlüsseln. Es ist ersichtlich, dass der Baum degeneriert und so die Laufzeit sehr stark ansteigt. 5.2.2 Rot-Schwarz-Bäume Auch diese Impelementierung basiert wiederum auf dem Pseudo-Code in [CLRS09]. Dieser entspricht dem des natürlichen binären Suchbaums, erweitert um das Setzen der Farbe eines Knoten und der Funktionalität zum Balancieren. Komprimierung der Farbinformation Wie bereits im Abschnitt 2.2.1 angedacht, wurde die Komprimierung der Farbinformation implementiert. Die verwendete Implementierung wird in A.1.1 gezeigt. Bei den Benchmarks zum Vergleich von Rot-Schwarz-Bäumen wurden jeweils 1 Million Knoten getestet. Das heißt beim Einfügen wurden diese Schlüssel in den leeren Baum eingefügt und bei der Suche zuerst in der 22 KAPITEL 5. BENCHMARKS Abbildung 5.2: Einfügen in aufsteigender Reihenfolge in einem natürlichen Binärbaum. angegebenen Reihenfolge eingefügt und dann wie angegeben gesucht. Es wurden jeweils alle eingefügten Schlüssel gesucht. Beim Einfügen ist die Performance durch das Decodieren des Elternknotens schlechter. Man sieht jedoch, dass dieser Unterschied beim Einfügen in zufälliger Reihenfolge, wie in Abbildung 5.5 ersichtlich, geringen ausfällt, da weniger Decodiervorgänge vorgenommen werden müssen, als beim Einfügen in aufsteigender Reihenfolge (Abbildung 5.6). Bei der Suche kann die kleinere Größe der Knoten Vorteile haben. Da bei der Suche kein Decodieren der ID des Vaters nötig ist, lässt sich das nachvollziehen. In den folgenden Benchmarks wird nur mehr der Rot-Schwarz-Baum mit aktivierter Kompression verwendet. 23 KAPITEL 5. BENCHMARKS Abbildung 5.3: Suchen in aufsteigender Reihenfolge. Schlüssel wurden aufsteigend eingefügt. Abbildung 5.4: Suchen in zufälliger Reihenfolge. Schlüssel wurden in aufsteigender Reihenfolge eingefügt. 24 KAPITEL 5. BENCHMARKS Abbildung 5.5: Einfügen in zufälliger Reihenfolge. Abbildung 5.6: Einfügen in aufsteigender Reihenfolge. 25 KAPITEL 5. BENCHMARKS Prefetching Beim Binärbaum besteht nur die Möglichkeit, dass entweder der linke oder der rechte Teilbaum besucht werden muss. Es wurde versucht vor dem Testen, ob die Operation im linken oder rechten Teilbaum fortgesetzt wird, bereits beide Knoten in den Cache zu laden. Prefetching mit der GCC-Funktion builtin prefetch brachte keine Performancevorteile. Der Grund dafür dürfte sein, dass die Zeitspanne zwischen dem Funktionsaufruf und dem Speicherzugriff zu kurz ist und so die benötigten Daten noch nicht im Cache liegen. Außerdem müssen die absoluten Adressen beider Nachfolger berechnet werden, obwohl man dann nur eine wirklich benötigt. 5.2.3 B-Baum Verschiedene Verzweigungsgrade Die Implementierung ermöglicht es die Größe eines Knotens in Byte im Header der Datenstruktur festzulegen. Besonders eignen sich Vielfache der Cache-Line-Size des jeweiligen Systems. Bei aktuellen Prozessoren sind dies meistens 64 Byte. Es stellte sich eine Größe von 512 Byte als am schnellsten heraus. Dies entspricht bei 8-Byte-Schlüsseln und Adressen einem Verzweigungsgrad von 32. Bei der Suche in einem Knoten handelt es sich um eine lineare Suche. Möglicherweise würde eine Binärsuche in Kombination mit größeren Knoten Performancevorteile bringen. Dieses Ergebnis deckt sich auch mit den Resultaten des Artikels [HP03]. Dort wurden zwar Knotengrößen für B+ -Bäume untersucht. Es darf aber angenommen werden, dass diese Ergebnisse auch für B-Bäume gelten. Prefetching des gesamten Knotens Da die Cache-Line-Size gewöhnlich kleiner als die Größe des gesamten Knotens ist, macht Prefetching Sinn. Beispielsweise bei einer Knotengröße von 256 Bytes und einer Cache-Line von 64 Byte wird die builtin prefetch Funktion viermal jeweils versetzt um 64 Bytes aufgerufen. Benchmarks zeigen, dass Geschwindigkeitssteigerungen von 10% möglich sind. 26 KAPITEL 5. BENCHMARKS Komprimierung 5.3 Ausgwählte Resultate Abbildung 5.7: Einfügen von 10 Millionen aufsteigenden Schlüsseln. Abbildung 5.8: Einfügen von 10 Millionen Schlüsseln in zufälliger Reihenfolge. 27 KAPITEL 5. BENCHMARKS Abbildung 5.7 und 5.8 zeigen das Einfügen in aufsteigender bzw. zufälliger Reihenfolge. Man sieht, dass beim zufälligen Einfügen die Kurve bei den B-Bäumen gleichmäßiger verläuft. Der Grund dafür ist, dass der Füllgrad höher ist und dadurch die Datenstruktur eine Ebene niedriger ist. Der Knick deutet darauf hin, dass hier ein Wachsen in der Höhe stattgefunden hat. Die Performance des B-Baums ist mehr als 100% schneller als die des Rot-Schwarz-Baumes. Abbildung 5.9: Suchen von 1 Million Schlüsseln in zufälliger Reihenfolge. Schlüssel wurden auch in zufälliger Reihenfolge eingefügt. Die Abbildungen 5.9 und 5.10 bilden das Suchen zufälliger bzw. aufsteigender Reihenfolge ab. Es werden jeweils 1 Million Schlüssel gesucht. Die Datenstruktur werden im Vorhinein mit 1 Million Schlüssel, jeweils in zufälliger Reihenfolge befüllt. Es zeigt sich wieder ein Performancevorteil von mehr als 100% bei den BBäumen gegenüber dem Rot-Schwarz-Baum. Der komprimierte B-Baum hat in beiden Fällen Performancevorteile gegenüber der unkomprimierten Variante 28 KAPITEL 5. BENCHMARKS Abbildung 5.10: Suchen von 1 Million Schlüsseln in aufsteigender Reihenfolge. Schlüssel wurden in aufsteigender Reihenfolge eingefügt. 29 Kapitel 6 Zusammenfassung und Ausblick Diese Arbeit evaluiert verschiedene Indexstrukturen sowohl theoretisch, als auch durch Benchmarks. Durch die Benchmarks wurde aufgezeigt, dass die Cache-Optimierung, auch durch Kompression, großen Einfluss auf die Performance hat. Aufwendige Algorithmen mit mehr Instruktionen fallen durch die Memory-Wall, also den kleinen Fortschritt bei Speicherlatenzen gegenüber den raschen Verbesserungen der Prozessoren, kaum ins Gewicht. Der komprimierte B-Baum stellte sich als schnellste Datenstruktur heraus. Zurückzuführen ist das auf die gute CacheAusnützung, welche sich bei der Suche positiv auswirkt. Durch die Komprimierung erhöht sich aber auch der Verzweigungsgrad und die Häufigkeit von Restrukturierungen bei Update-Operationen kann gesenkt werden. Die verwendete Komprimierung basiert darauf, dass anstatt von absoluten Werten nur Differenzwerte gespeichert werden. Der absolute Wert berechnet sich über die Summe der relativen Werte auf dem Pfad zum jeweiligen Schlüssel. Man könnte den Kompressionsgrad noch weiter erhöhen indem man nicht nur relative Werte im Bezug zur Wurzel verwendet, sondern dieses Verfahren nur für einen Schlüssel eines Knotens anwendet und die restlichen Schlüssel wiederum relativ zu diesem kodiert. Gerade bei Datensätzen mit kleinen Differenzen zwischen den Schlüsseln kann sich so ein sehr hoher Kompressionsgrad ergeben. 30 Appendix A.1 A.1.1 Implementierungsdetails Komprimierung der Farbinformation des Rot-SchwarzBaums Für die Farbe wird ein Bit benötigt. Dieses wird im niederstwertigen Bit des Pointers zum Vater gespeichert. Die Vorgehensweise mittels Bitoperationen wird in Listing 1 gezeigt. Bei color handelt es sich um einen enum mit den Werten 0 für rot und 1 für schwarz. Listing 1: Mitspeichern der Farbinformation in einem 64-Bit Pointer c o l o r g e t c o l o r ( struct p a r e n t n o d e ∗ x ) { return x−>p a r e n t & 1 ; } s i z e t g e t p a r e n t i d ( struct p a r e n t n o d e ∗ x ) { return ( x−>p a r e n t >> 1 ; } void s e t p a r e n t i d ( struct p a r e n t n o d e ∗ x , s i z e t i d ) { x−>p a r e n t = ( x−>p a r e n t & 1 ) | ( i d << 1 ) ; } void s e t c o l o r ( struct p a r e n t n o d e ∗ x , c o l o r c o l o r ) { x−>p a r e n t = ( x−>p a r e n t & ˜ 1 ) | c o l o r ; } A.2 Speicherverwaltung Die Datenstrukturen verwenden eine eigene Speicherverwaltung in Form einer Freispeicherliste. Das hat den Vorteil, dass die Allozierung von Speicher weniger Einfluss auf die Laufzeit der Datenstrukturen hat. Dadurch werden die Ergebnisse der Benchmarks reproduzierbarer. Ein weiterer Vorteil ist, dass die gesamte Datenstruktur in einem einzigen Speicherblock liegt und so leicht in eine Datei gespeichert werden kann. 31 APPENDIX A.2. SPEICHERVERWALTUNG Diese Datei kann auch mittels mmap eingebunden werden, wodurch die persistente Speicherung ermöglicht wird. Eine weitere Voraussetzung, dass dies funktioniert ist die Verwendung von relativen Adressen. Außerdem kann eine Speicherverwaltungsinstanz mehrere Datenstrukturen beinhalten, jedoch müssen diese implementierungsbedingt vom selben Typ sein. A.2.1 Anlegen des Speichers und von Datenstrukturen Speicher wird mit der memory malloc-Funktion angefordert. Beim Funktionsaufruf wird angegeben wie viele Schlüssel mindestens im initial angelegten Speicher Platz finden sollen. Die so erzeugte memory-Struktur ist der Rückgabewert der memory malloc-Funkion und muss bei den Operationen auf der Datenstruktur referenziert werden. Sollte der Speicher zu klein sein, wird dieser automatisch vergrößert. Die gewünschte Reallozierungsstrategie kann durch ein Präprozessormakro frei festgelegt werden. Die weiteren Funktionen, welche die Speicherverwaltung zur Verfügung stellt, sind im memorymanagement.h-Header definiert: s i z e t memory get ( struct memory∗∗ mem) b o o l m e m or y f r ee ( struct memory∗∗ mem, s i z e t s e g i d ) void ∗ memory dref ( struct memory∗∗ mem, s i z e t i d ) struct d a t a s t r u c t u r e ∗ m e m o r y a d d d a t a s t r u c t u r e ( struct memory∗∗ mem) b o o l m e m o r y d e s t r o y d a t a s t r u c t u r e ( struct memory∗∗ mem, s i z e t i d ) b o o l memory save ( struct memory∗∗ mem) b o o l memory saveas ( struct memory∗∗ mem, char∗ path ) b o o l m e m o r y s e t p a t h ( struct memory∗∗ mem ptr ptr , char∗ path ) struct memory∗∗ memory load ( char∗ path , enum memory mode mode ) struct d a t a s t r u c t u r e ∗ m e m o r y l o a d d a t a s t r u c t u r e ( struct memory∗∗ mem, s i z e t i d ) struct d a t a s t r u c t u r e ∗ m e m o r y l o a d d a t a s t r u c t u r e s ( struct memory∗∗ mem) Will man eine Datenstruktur im Speicher anlegen, geschieht dies mit der memory add datastructure-Funktion. Diese Funktion liefert dann einen Pointer auf die datastructure-Struktur zurück. Um eine Datenstruktur zu löschen wird die memory destroy datastructure-Funktion benützt. Die Funktionen memory get und memory free fordern ein Speichersegment der beim Erstellen des Speichers definierten Größe an bzw. geben dieses frei. Da die Speicherverwaltung mit relativen Adressen arbeitet, werden diese als IDs (vom Typ size t) bezeichnet. Um solche IDs zu absoluten Adressen aufzulösen steht die memory dref-Funktion bereit. Will man eine Datenstruktur aus dem Speicher löschen, macht man dies mit der memory destroy datastructure-Funktion. Die verbleibenden Funktionen werden zum Speichern und laden einer Datenstruktur bzw. des gesamten Speichersegmentes benützt. Die Funktionen memory save bzw. memory save as veranlassen das Speichern des 32 APPENDIX A.2. SPEICHERVERWALTUNG Segments in eine Datei. Dabei setzt die memory save-Funktion voraus, dass bereits eine Datei mit memory set path gesetzt worden ist. Um eine so gespeichterte Datei wieder zu laden steht die memory loadFunktion zur Verfügung. Der Parameter mode kann entweder in memory oder mapped sein. Ersteres bedeutet, dass die Datei komplett in den Arbeitsspeicher geladen wird. Im Gegensatz dazu wird die Datei bei mapped mittels mmap eingebunden. Dadurch werden Veränderungen an den Datenstrukturen automatisch persistent in die Datei geschrieben, während bei in memory dieser Schritt explizit erfolgen muss. Die Abbildung 1 zeigt schematisch den Aufbau des Speichers. Die memoryStruktur wird vom Bereich für die Speicherung der einzelnen Baumknoten gefolgt. Am Ende des Speichers befinden sich die datastructureStrukturen, welche Metadaten über die verschiedenen Datenstrukturen enthalten. Abbildung 1: Der Aufbau der Speicherverwaltung mit einer bzw. zwei Datenstrukturen. 33 Literaturverzeichnis [And93] A. Andersson: Balanced Search Trees Made Simple, Proceedings of the Third Workshop on Algorithms and Data Structures, WADS ’93, Springer-Verlag, London, UK, pages 60–71. [AT07] A. Andersson and M. Thorup: Dynamic ordered sets with exponential search trees, J. ACM, volume 54. [Bay72] R. Bayer: Symmetric binary B-Trees: Data structure and maintenance algorithms, Acta Informatica, volume 1, (1972), pages 290–306, 10.1007/BF00289509. [CLRS09] T. H. Cormen, C. E. Leiserson, R. L. Rivest and C. Stein: Introduction to Algorithms, Third Edition, The MIT Press, 3rd edition, 2009. [GR93] I. Galperin and R. L. Rivest: Scapegoat trees, Proceedings of the fourth annual ACM-SIAM Symposium on Discrete algorithms, SODA ’93, Society for Industrial and Applied Mathematics, Philadelphia, PA, USA, pages 165–174. [GS78] L. J. Guibas and R. Sedgewick: A dichromatic framework for balanced trees, Foundations of Computer Science, Annual IEEE Symposium on, volume 0, (1978), pages 8–21. [HP03] R. A. Hankins and J. M. Patel: Effect of node size on the performance of cache-conscious B+-trees, SIGMETRICS Perform. Eval. Rev., volume 31(1), (2003), pages 283–294. [LA05] C. Lattner and V. S. Adve: Transparent pointer compression for linked data structures, Proceedings of the 2005 workshop on Memory system performance, MSP ’05, ACM, New York, NY, USA, pages 24–35. [LC86] T. J. Lehman and M. J. Carey: A Study of Index Structures for Main Memory Database Management Systems, Proceedings of the 12th International Conference on Very Large 35 APPENDIX LITERATURVERZEICHNIS Data Bases, VLDB ’86, Morgan Kaufmann Publishers Inc., San Francisco, CA, USA, pages 294–303. [LNT00] H. Lu, Y. Y. Ng and Z. Tian: T-Tree or B-Tree: Main Memory Database Index Structure Revisited, Proceedings of the Australasian Database Conference, ADC ’00, IEEE Computer Society, Washington, DC, USA, pages 65–. [LSLC07] I.-h. Lee, J. Shim, S.-g. Lee and J. Chun: CST-trees: cache sensitive t-trees, Proceedings of the 12th international conference on Database systems for advanced applications, DASFAA’07, Springer-Verlag, Berlin, Heidelberg, pages 398–409. [OW02] T. Ottmann and P. Widmayer: Algorithmen und Datenstrukturen, Spektrum Akademischer Verlag, 2002. [RR99] J. Rao and K. A. Ross: Cache Conscious Indexing for Decision-Support in Main Memory, M. P. Atkinson, M. E. Orlowska, P. Valduriez, S. B. Zdonik and M. L. Brodie (eds.), VLDB’99, Proceedings of 25th International Conference on Very Large Data Bases, September 7-10, 1999, Edinburgh, Scotland, UK, Morgan Kaufmann, pages 78–89. [RR00] J. Rao and K. A. Ross: Making B+- trees cache conscious in main memory, SIGMOD Rec., volume 29, (2000), pages 475–486. [Sed08] Sedgewick: Left-Leaning Red-Black Trees, 2008, URL http: //www.cs.princeton.edu/~rs/talks/LLRB/LLRB.pdf. [WM95] W. A. Wulf and S. A. McKee: Hitting the memory wall: implications of the obvious, SIGARCH Comput. Archit. News, volume 23, (1995), pages 20–24. 36