Re-implementation of Lock-free Contention Adapting Search Trees Using MRLock

To explore multi-core programming, we re-implement the Lock Free Contention Adapting Search Tree. We follow the structure of the original, using immutable treaps as leaf nodes implemented with an array for better performance with memory caching. The initial re-implementation uses coarsegrained locking with MRLock to provide thread-safety. This will be extended to be lock-free in a future publication.


Contents
The Lock-free Contention Adapting Search Tree (LFCA Search Tree) [1] is composed of route nodes and base nodes. Route nodes allow searching through the tree, similar to Binary Search Trees (BSTs). These nodes contain a value and a pointer to the node's left and right child. All elements within the left subtree are less than or equal to the route node's value, and all elements within the right subtree tree are greater than the route node's value. Base nodes store a reference to an immutable data structure (in our case, a treap), which stores the values contained in the LFCA tree. The treap will be explained further in Section 3. Figure 1 shows the structure of the LFCA tree. The LFCA tree our report is based on supports lock-free insert, remove, and range-query, as well as wait-free lookup. Our simplified re-implementation of the LFCA tree provides thread-safety for all operations through coarse-grained locking using the Multi-Resource Lock (MRLock) library [2]. Only one method can be called at a time by a single thread, and all other threads must wait. This is done by creating a single resource which represents the entire tree. All methods must acquire this resource before executing. A wrapper class was created for using MRLock which uses the programming technique Resource Acquisition Is Initialization (RAII) [3]. The initialization of the wrapper class acquires the lock and its destruction releases the lock. This simplifies the use of the lock and avoids issues with unlocking when branching is involved, since the lock will always be released when the method is exited.
Coarse-grained locking is simple to implement and verify, but provides high contention on the tree due to all operations needing the same lock. This contention will be reduced when the tree is converted into a lock-free version in the future.
The locked version does not have any contention-adapting properties, as this is irrelevant when no two threads can be accessing the tree at once. It also only allows storing integers in the tree, but could be extended to store arbirary data.

Key Methods
Our re-implementation supports the same methods as the original: Insert, Remove, Range-Query, and Lookup.

Insert
The insert operation traverses the route nodes using binary search until a base node is found. The value is inserted into the treap the base node is pointing to.
After adding the value to the treap, the size of the treap is checked. If the treap's size is above a threshold (>= 64 in our case), the treap is split in two. The base node is converted into a route node whose value is the splitting point of the treap. The left and right children of the new route node are the left and right side of the split treap, respectively.

Remove
The remove operation traverses the route nodes using binary search until a base node is found. The value is removed from the treap the base node is pointing to, if it exists.
After potentially removing the value from the treap, a check is made to determine whether to perform a merge. If the base node's sibling (the other side of the base node's parent route node) is also a base node, then the size of the two treaps is checked. If the combined size of the two treaps is below a threshold (< 32 in our case), then the two treaps are merged together. The parent route node is converted into a base node which points to the new treap.

Lookup
The lookup operation traverses the route nodes using binary search until a base node is found, then determines whether the value is located in the base node's treap.

Range Query
The range-query operation traverses the route nodes using a depth-first search to locate all base nodes that may contain values within the range. For each of these nodes, the referenced treaps are searched for all values within the range. These values are combined into a single vector and returned.

Treap Leaf Nodes
The leaf nodes of our search tree are implemented using treaps. A treap is a data structure that combines the properties of binary search trees (BSTs) and heaps. BST ordering allows for fast lookups, while heap ordering is used to balance the tree. Nodes are given random weights which, on average, result in a treap that is balanced. The balanced treap has a depth of O(log n), which allows for O(log n) lookup. Our treaps have a maximum size of 64, which was the size used in [1].
All operations on the treap are immutable operations, meaning that the original treap is not modified. Instead, new treaps are created and returned, which contain the modifications. This is not necessary for the locked version of the LFCA tree, but will be required for a lock-free implementation.
An unusual property of these treaps is that they are implemented using an array. Rather than nodes being linked to one another by pointers, the nodes are linked using local array indices. By keeping the nodes nearby in memory, multiple accesses to the same treap will be likely to result in many cache hits. This increases the performance of lookups. Another benefit is that the treaps can easilly be copied withoout needing to allocate and link every node. Only the contents of the node array need to be copied. This property is less important for the locked version of the LFCA tree, but will be very important for the lock-free version, as a copy of the treap will be made for every modification due to the immutability. Figure 2 provides a graphical representation of the structure of the treap.

Treap Operations
The following is a list of operations supported by the treaps. These operations were implemented based on pseudocode from [4].
• Lookup: Determines if a value is in the treap.
• Insert: Inserts a value into the treap. The value is inserted into the last free index in the node array.
• Remove: Removes a value from the treap. In order to restore the node array structure, the last node in the array is transfered into the location where the removed node was, adjusting any references to the node as needed. The transfer is a constant-time operation with little overhead.
• Range-query: Returns all values in the treap within a range.
• Split: Splits the treap into two approximately equal-sized treaps. The treaps are split at the median of the contained values, giving a left and right treap. These contain all values less than or equal to the median, and all values greater than the median, respectively.
• Merge: Merges two treaps (a left and right treap) together into a new treap. The combined size of the two treaps must not be larger than the maximum size of a treap. All values in the left treap must be smaller than the values in the right treap.
The split and merge operations are performed using a special node called the Control Node. When splitting a treap, this node is inserted into the treap with the median value and pulled up to the root. The left and right children of the Control Node become the left and right treaps. When merging two treaps, the process is performed in reverse. The Control Node is added as the root of the new treap, and its left and right children are the left and right treaps. The control node is moved down to become a leaf node, and cut off.
The process for moving nodes up and down and the details for maintaining BST and heap ordering when inserting and removing are common to the treap data structure, and will not be discussed.