Inside The JeMalloc. Core Data Structures: Pairing Heap & Bitmap Tree

image



The topic of Allocators often pops up on the Internet: indeed, an allocator is a kind of cornerstone, the heart of any application. In this series of posts I want to talk in detail about one very entertaining and eminent allocator - JeMalloc , supported and developed by Facebook and used, for example, in bionic [Android] lib C.



On the network, I could not find any details that fully reveal the soul of this allocator, which ultimately affected the impossibility of making any conclusions about the applicability of JeMalloc in solving a particular problem. There was a lot of material and, in order to read it was not tiring, I suggest starting with the basics: The Basic Data Structures used in JeMalloc.



Under the cat, I’m talking about Pairing Heap and Bitmap Tree , which form the foundation of JeMalloc. At this stage, I do not touch on the topic of multithreading and Fine Grained Locking , however, continuing the series of posts, I will definitely tell you about these things, for the sake of which, in fact, various kinds of Exotics are created, in particular the one described below.



Bitmap_s is a tree-like data structure that allows you to:





It is reasonable to represent bitmap_t in two arrays: the first, the dimension equal to the number of all tree nodes — to store the tree nodes, the second, the tree height dimension — to store the offset of the beginning of each level in the array of tree nodes. With this method of specifying a tree, the root element can go first, and then, in sequence, the nodes of each of the levels. However, JeMalloc authors store the root element last in the array, and in front of it are the nodes of the underlying tree levels.



As an illustrative example, take a sequence of 92 bits and K = 32 mind. We assume that the state is 'free' - denoted by a single bit, and the state 'busy' - zero. Assume also that in the original bit sequence, the bit with index 1 (counting from zero, from right to left in the figure) is set to the busy state. Then bitmap_t for such a bit sequence will look like the image below:



image



From the figure we see that the resulting tree has only two levels. The root level contains 3 unit bits, which indicates the presence of both free and occupied bits in each of the child nodes of the corresponding bit. In the lower right corner, you can see the Tree view in two arrays: all tree nodes and offsets of each level in the node array.



Assuming that bitmap_t is represented by two arrays (a data array and an array of tree level offsets in the data array), we describe the operation of searching for the first least significant bit in a given bitmap_t:





Pairing Heap is a heap-like tree data structure that allows:





More formally speaking, Pairing Heap is an arbitrary tree with a dedicated root that satisfies the properties of the heap (the key of each vertex is no less / no more than the key of its parent).

A typical Pairing Heap in which the value of the child node is less than the value of the parent node (aka Min Pairing Heap) looks something like this:



image



As a rule, Pairing-Heap is represented in the computer’s memory in a completely different way: each node of the Tree stores 3 pointers:





If any of the indicated nodes is missing, the corresponding node pointer is nullified.

For the heap presented in the figure above, we get the following representation:



image



Here, the pointer to the leftmost child node is indicated by a red dotted arrow, the pointer to the right brother of the node is blue, and the pointer to the left brother is gray. The solid arrow shows the Pairing Heap representation in a tree-like form that is common for humans.



Please note the following two facts:





Next, we list the algorithm of the main operations in the Min Pairing-Heap :





Consider the merge algorithm using a specific example:



image



Here, the heap with the root '4' joins the heap with the root '1' (1 <4), becoming its left-most child node. The pointer to the right brother of the node '4' - is updated, as well as the pointer to the left brother of the node '8'.





A good example of the process described above:



image





However, our acquaintance with Pairing Heap does not end there: Pairing Heap allows you to perform some operations not immediately, but only when the need arises for them. In other words, Pairing Heap allows you to perform operations 'Lazily' , thereby lowering the amortized cost of some of them.

Suppose we made K insertions and K deletes in Pairing Heap. Obviously, the result of these operations becomes necessary only when we request the minimum element from the heap.

Let's consider how the algorithm of actions of the previously described operations will change during their Lazy implementation:



Taking advantage of the fact that the Kuchi root has at least two null pointers, we will use one of them to represent the head of a doubly linked list (hereinafter auxList ) of elements inserted into the heap, each of which will be considered as the root of an independent Pairing Heap. Then:







image





Actually, the use of the Lazy Pairing Heap implementation reduces the amortized cost of insertion and removal of arbitrary nodes in the Pairing Heap: from O (log N) to O (1).



That's all, and below you will find a list of useful links and resources:



[0] JeMalloc Github Page

[1] Original article on Pairing Heaps

[2] Original article on Lazy Implementation Pairing Heaps

[3] Telegram channel about code optimizations, C ++, Asm, 'low level' and nothing more than that!



To be continued…



All Articles