This paper was converted on www.awesomepapers.org from LaTeX by an anonymous user.
Want to know more? Visit the Converter page.

A Lock-free Binary Trie

Jeremy Ko, University of Toronto, [email protected]
Abstract

A binary trie is a sequential data structure for a dynamic set on the universe {0,,u1}\{0,\dots,u-1\} supporting Search with O(1)O(1) worst-case step complexity, and Insert, Delete, and Predecessor operations with O(logu)O(\log u) worst-case step complexity.

We give a wait-free implementation of a relaxed binary trie, using read, write, CAS, and (logu\log u)-bit AND operations. It supports all operations with the same worst-case step complexity as the sequential binary trie. However, Predecessor operations may not return a key when there are concurrent update operations. We use this as a component of a lock-free, linearizable implementation of a binary trie. It supports Search with O(1)O(1) worst-case step complexity and Insert, Delete and Predecessor with O(c2+logu)O(c^{2}+\log u) amortized step complexity, where cc is a measure of the contention.

A lock-free binary trie is challenging to implement as compared to many other lock-free data structures because Insert and Delete operations perform a non-constant number of modifications to the binary trie in the worst-case to ensure the correctness of Predecessor operations.

1 Introduction

Finding the predecessor of a key in a dynamic set is a fundamental problem with wide-ranging applications in sorting, approximate matching and nearest neighbour algorithms. Data structures supporting Predecessor can be used to design efficient priority queues and mergeable heaps [42], and have applications in IP routing [15] and bioinformatics [3, 31].

A binary trie is a simple sequential data structure that maintains a dynamic set of keys SS from the universe U={0,,u1}U=\{0,\dots,u-1\}. Predecessor(y)(y) returns the largest key in SS less than key yy, or 1-1 if there is no key smaller than yy in SS. It supports Search with O(1)O(1) worst-case step complexity and Insert, Delete, and Predecessor with O(logu)O(\log u) worst-case step complexity. It has Θ(u)\Theta(u) space complexity.

The idea of a binary trie is to represent the prefixes of keys in UU in a sequence of b+1b+1 arrays, Di\mathit{D}_{i}, for 0ib0\leq i\leq b, where b=log2ub=\lceil\log_{2}u\rceil. Each array Di\mathit{D}_{i} has length 2i2^{i} and is indexed by the bit strings {0,1}i\{0,1\}^{i}. The array entry Di[x]\mathit{D}_{i}[x] stores the bit 1 if xx is the prefix of length ii of some key in SS, and 0 otherwise. The sequence of arrays implicitly forms a perfect binary tree. The array entry Di[x]\mathit{D}_{i}[x] represents the node at depth ii with length ii prefix xx. Its left child is the node represented by Di+1[x0]\mathit{D}_{i+1}[x\cdot 0] and its right child is the node represented by Di+1[x1]\mathit{D}_{i+1}[x\cdot 1]. Note that Db\mathit{D}_{b} (which represents the leaves of the binary trie) is a direct access table describing the set SUS\subseteq U. An example of a binary trie is shown in Figure 1.

A Search(x)(x) operation reads Db[x]\mathit{D}_{b}[x] and returns True if Db[x]\mathit{D}_{b}[x] has value 1, and False otherwise. An Insert(x)(x) operation sets the bits of the nodes on the path from the leaf Db[x]\mathit{D}_{b}[x] to the root to 1. A Delete(x)(x) operation begins by setting Db[x]\mathit{D}_{b}[x] to 0. It then traverses up the trie starting at Db[x]\mathit{D}_{b}[x], setting the value of the parent of the current node to 0 if both its children have value 0. A Predecessor(y)(y) operation pOppOp traverses up the trie starting from the leaf Db[y]\mathit{D}_{b}[y] to the root. If the left child of each node on this path either has value 0 or is also on this path, then pOppOp returns 1-1. Otherwise, consider the first node on this path whose left child tt has value 1 and is not on this path. Then starting from tt, pOppOp traverses down the right-most path of nodes with value 1 until it reaches a leaf Db[w]\mathit{D}_{b}[w], and returns ww.

Refer to caption
Figure 1: A sequential binary trie for the set S={0,2}S=\{0,2\} from a universe U={0,1,2,3}U=\{0,1,2,3\}.

More complicated variants of sequential binary tries exist, such as van Emde Boas tries [41], x-fast tries and y-fast tries [44]. Compared to binary tries, they improve the worst-case complexity of predecessor operations. Both x-fast tries and y-fast tries use hashing to improve the space complexity, and hence are not deterministic. Furthermore, none of these variants support constant time search. One motivation for studying lock-free binary tries is as a step towards efficient lock-free implementations of these data structures.

Universal constructions provide a framework to give (often inefficient) implementations of concurrent data structures from sequential specifications. A recent universal construction by Fatourou, Kallimanis, and Kanellou [19] can be used to implement a wait-free binary trie supporting operations with O(P+c¯(op)logu)O(P+\bar{c}(op)\cdot\log u) worst-case step complexity, where PP is the number of processes in the system, and c¯(op)\bar{c}(op), is the interval contention of opop. This is the number of Insert, Delete, and Predecessor operations concurrent with the operation opop. Prior to this work, there have been no lock-free implementations of a binary trie or any of its variants without using universal constructions.

There are many lock-free data structures that directly implement a dynamic set, including variations of linked lists, balanced binary search trees, skip lists [36], and hash tables. There is also a randomized, lock-free implementation of a skip trie[33]. We discuss these data structures in more detail in Section 3.

Our contribution: We give a lock-free implementation of a binary trie using registers, compare-and-swap (CAS) objects, and (b+1b+1)-bounded min-registers. A min-write on a (b+1b+1)-bit memory location can be easily implemented using a single (b+1b+1)-bit AND operation, so all these shared objects are supported in hardware. The amortized step complexity of our lock-free implementation of the binary trie is expressed using two other measures of contention. For an operation opop, the point contention of opop, denoted c˙(op)\dot{c}(op), is the maximum number of concurrent Insert, Delete, and Predecessor operations at some point during opop. The overlapping-interval contention [33] of opop, denoted, c~(op)\tilde{c}(op) is the maximum interval contention of all update operations concurrent with opop. Our implementation supports Search with O(1)O(1) worst-case step complexity, and Insert with O(c˙(op)2+logu)O(\dot{c}(op)^{2}+\log u) amortized step complexity, and Delete and Predecessor operations with O(c˙(op)2+c~(op)+logu)O(\dot{c}(op)^{2}+\tilde{c}(op)+\log u) amortized step complexity. In a configuration CC where there are c˙(C)\dot{c}(C) concurrent Insert, Delete, and Predecessor operations, the implementation uses O(u+c˙(C)2)O(u+\dot{c}(C)^{2}) space. Our data structure consists of a relaxed binary trie, as well as auxiliary lock-free linked lists. Our goal was to maintain the O(1)O(1) worst-case step complexity of Search, while avoiding O(c¯(op)logu)O(\bar{c}(op)\cdot\log u) terms in the amortized step complexity of the other operations seen in universal constructions of a binary trie. Our algorithms to update the bits of the relaxed binary trie and traverse the relaxed binary trie finish in O(logu)O(\log u) steps in the worst-case, and hence are wait-free. The other terms in the amortized step complexity are from updating and traversing the auxiliary lock-free linked lists.

Techniques: A linearizable implementation of a concurrent data structure requires that all operations on the data structure appear to happen atomically. In our relaxed binary trie, predecessor operations are not linearizable. We relax the properties maintained by the binary trie, so that the bit stored in each internal binary trie node does not always have to be accurate. At a high-level, we ensure that the bit at a node is accurate when there are no active update operations whose input key is a leaf of the subtrie rooted at the node. This allows us to design an efficient, wait-free algorithm for modifying the bits along a path in the relaxed binary trie.

Other lock-free data structures use the idea of relaxing properties maintained by the data structure. For example, in lock-free balanced binary search trees [11], the balance conditions are often relaxed. This allows the tree to be temporarily unbalanced, provided there are active update operations. A node can be inserted into a tree by updating a single pointer. Following this, tree rotations may be performed, but they are only used to improve the efficiency of search operations. Lock-free skip lists [21] relax the properties about the heights of towers of nodes. A new node is inserted into the bottom level of a skip list using a single pointer update. Modifications that add nodes into the linked lists at higher levels only improve the efficiency of search operations.

Relaxing the properties of the binary trie is more complicated than these two examples because it affects correctness, not just efficiency. For a predecessor operation to traverse a binary trie using the sequential algorithm, the bit of each node must be the logical OR of its children. This is not necessarily the case in our relaxed binary trie. For example, a node in the relaxed binary trie with value 1 may have two children which each have value 0.

The second way our algorithm is different from other data structures is how operations help each other complete. Typical lock-free data structures, including those based on universal constructions, use helping when concurrent update operations require modifying the same part of a data structure: a process may help a different operation complete by making modifications to the data structure on the other operation’s behalf. This technique is efficient when a small, constant number of modifications to the data structure need to be done atomically. For example, many lock-free implementations of linked lists and binary search trees can insert a new node by repeatedly attempting CAS to modify a single pointer. For a binary trie, update operations require updating O(logu)O(\log u) bits on the path from a leaf to the root. An operation that helps all these updates complete would have O(c˙(op)logu)O(\dot{c}(op)\cdot\log u) amortized step complexity.

Predecessor operations that cannot traverse a path through the relaxed binary trie do not help concurrent update operations complete. For our linearizable, lock-free binary trie, our approach is to have update operations and predecessor operations announce themselves in an update announcement linked list and a predecessor announcement linked list, respectively. We guarantee that a predecessor operation will either learn about concurrent update operations by traversing the update announcement linked list, or it will be notified by concurrent update operations via the predecessor announcement linked list. A predecessor operation uses this information to determine a correct return value, especially when it cannot complete its traversal of the relaxed binary trie. This is in contrast to other lock-free data structures that typically announce operations so that they can be completed by concurrent operations in case the invoking processes crash.

Section 2 describes the asynchronous shared memory model. In Section 3, we describe related lock-free data structures and compare them to our lock-free binary trie. In Section 4 we give the specification of a relaxed binary trie, give a high-level description of our wait-free implementation, present the pseudocode of the algorithm, and prove it correct. In Section 5, we give a high-level description of our implementation of a lock-free binary trie, present the pseudocode of the algorithm and a more detailed explanation, prove it is linearizable, and analyze its amortized step complexity. We conclude in Section 6. .

2 Model

Throughout this paper, we use an asynchronous shared memory model. Shared memory consists of a collection of shared objects accessible by all processes in a system. The primitives supported by these objects are performed atomically. A register is an object supporting Write(w)(w), which stores ww into the object, and Read()(), which returns the value stored in the object. CAS(r,old,new)(r,old,new) compares the value stored in object rr with the value oldold. If the two values are the same, the value stored in rr is replaced with the value newnew and True is returned; otherwise False is returned. A min-register is an object that stores a value, and supports Read()(), which returns the value of the object, and MinWrite(w)(w), which changes the value of the object to ww if ww is smaller than its previous value.

A configuration of a system consists of the values of all shared objects and the states of all processes. A step by a process either accesses or modifies a shared object, and can also change the state of the process. An execution is an alternating sequence of configurations and steps, starting with a configuration.

An abstract data type is a collection of objects and types of operations that satisfies certain properties. A concurrent data structure for the abstract data type provides representations of the objects in shared memory and algorithms for the processes to perform operations of these types. An operation on a data structure by a process becomes active when the process performs the first step of its algorithm. The operation becomes inactive after the last step of the algorithm is performed by the process. This last step may include a response to the operation. The execution interval of the operation consists of all steps between its first and last step (which may include steps from processes performing other operations). In the initial configuration, the data structure is empty and there are no active operations.

We consider concurrent data structures that are linearizable [28], which means that its operations appear to occur atomically. One way to show that a concurrent data structure is linearizable is by defining a linearization function, which, for all executions of the data structure, maps all completed operations and a subset of the uncompleted operations in the execution to a step or configuration, called its linearization point. The linearization points of these operations must satisfy two properties. First, each linearized operation is mapped to a step or configuration within its execution interval. Second, the return value of the linearized operations must be the same as in the execution in which all the linearized operations are performed atomically in the order of their linearization points, no matter how operations with the same linearization point are ordered.

A concurrent data structure is strong linearizability if its linearization function satisfies an additional prefix preserving property: For all its executions α\alpha and for all prefixes α\alpha^{\prime} of α\alpha, if an operation is assigned a linearization point for α\alpha^{\prime}, then it is assigned the same linearization point for α\alpha, and if it is assigned a linearization point for α\alpha that occurs during α\alpha^{\prime}, then it is assigned the same linearization point for α\alpha^{\prime}. This means that the linearization point of each operation is determined as steps are taken in the execution and cannot depend on steps taken later in the execution. This definition, phrased somewhat differently, was introduced by Golab, Higham and Woelfel [23]. A concurrent data structure is strongly linearizable with respect to a set of operation types 𝒪\cal{O} if it has a linearization function that is only defined on operations whose types belong to 𝒪\cal{O}.

A lock-free implementation of a concurrent data structure guarantees that whenever there are active operations, one operation will eventually complete in a finite number of steps. However, the execution interval of any particular operation in an execution may be unbounded, provided other operations are completed. A wait-free implementation of a concurrent data structure guarantees that every operation completes within a finite number of steps by the process that invoked the operation.

The worst-case step complexity of an operation is the maximum number of steps taken by a process to perform any instance of this operation in any execution. The amortized step complexity of a data structure is the maximum number of steps in any execution consisting of operations on the data structure, divided by the number operations invoked in the execution. One can determine an upper bound on the amortized step complexity by assigning an amortized cost to each operation, such that for all possible executions α\alpha on the data structure, the total number of steps taken in α\alpha is at most the sum of the amortized costs of the operations in α\alpha.

3 Related Work

In this section, we discuss related lock-free data structures and the techniques used to implement them. We first describe simple lock-free linked lists, which are a component of our binary trie. We next describe search tree implementations supporting Predecessor, and discuss the general techniques used. Next we describe implementations of a Patricia trie and a skip trie. Finally, we discuss some universal constructions and general techniques for augmenting existing data structures.

There are many existing implementations of lock-free linked lists [24, 38, 40]. The implementation with best amortized step complexity is by Fomitchev and Ruppert [21]. It supports Insert, Delete, Predecessor, and Search operations opop with O(n(op)+c˙(op))O(n(op)+\dot{c}(op)) amortized step complexity, where n(op)n(op) is the number of nodes in the linked list at the start of opop. When n(op)n(op) is in O(c˙(op))O(\dot{c}(op)), such as in the case of our binary trie implementation, operations have O(c˙(op))O(\dot{c}(op)) amortized step complexity. This implementation uses flagging: a pointer that is flagged indicates an update operation wishes to modify it. Concurrent update operations that also need to modify this pointer must help the operation that flagged the pointer to complete before attempting to perform their own operation. After an update operation completes helping, it backtracks (using back pointers set in deleted nodes) to a suitable node in the linked list before restarting its own operation.

Ellen, Fatourou, Ruppert, and van Breugel [18] give the first provably correct lock-free implementation of an unbalanced binary search tree using CAS. Update operations are done by flagging a constant number of nodes, followed by a single pointer update. A flagged node contains a pointer to an operation record, which includes enough information about the update operation so that other processes can help it complete. Ellen, Fatourou, Helga, and Ruppert [17] improve the efficiency of this implementation so each operation opop has O(h(op)+c˙(op))O(h(op)+\dot{c}(op)) amortized step complexity, where h(op)h(op) is the height of the binary search tree when opop is invoked. This is done by allowing an update operation to backtrack along its search paths by keeping track of nodes it visited in a stack. There are many other implementations of lock-free unbalanced binary search trees [9, 14, 29, 32].

There are also many implementations of lock-free balanced binary search trees [5, 6, 8, 16]. Brown, Ellen, and Ruppert designed a lock-free balanced binary search tree [11] by implementing more powerful primitives, called LLX and SCX, from CAS [10]. These primitives are generalizations of LL and SC. SCX allows a single field to be updated by a process provided a specified set of nodes have not been modified since that process last performed LLX on them. Ko [30] shows that a version of their lock-free balanced binary search tree has good amortized step complexity.

Although a binary trie represents a perfect binary tree, the techniques we use to implement a binary trie are quite different than those that have been used to implement lock-free binary search trees. The update operations of a binary trie may require updating the bits of all nodes on the path from a leaf to the root. LLX and SCX do not facilitate this because SCX only updates a single field.

Brown, Prokopec, and Alistarh [12] give an implementation of an interpolation search tree supporting Search with O(P+logn(op))O(P+\log n(op)) amortized step complexity, and Insert and Delete with O(c¯avg(P+logn(op))O(\bar{c}_{avg}(P+\log n(op)) amortized step complexity, where c¯avg\bar{c}_{avg} is the average interval contention of the execution. Their data structure is a balanced search tree where nodes can have large degree. A node containing nn keys in its subtree is ideally balanced when it has n\sqrt{n} children, each of which contains n\sqrt{n} nodes in its subtree. Update operations help replace subtrees that become too unbalanced with ideally balanced subtrees. When the input distribution of keys is well-behaved, Search can be performed with O(P+loglogn(op))O(P+\log\log n(op)) expected amortized step complexity and update operations can be performed with O(c¯avg(P+loglogn(op))O(\bar{c}_{avg}(P+\log\log n(op)) expected amortized step complexity. Their implementation relies on the use of double-compare single-swap (DCSS). DCSS is not a primitive typically supported in hardware, although there exist implementations of DCSS from CAS [1, 22, 25].

Shafiei [37] gives an implementation of a Patricia trie. The data structure is similar to a binary trie, except that only internal nodes whose children both have value 1 are stored. In addition to Search, Insert and Delete, it supports Replace, which removes a key and adds another key in a possibly different part of the trie. Her implementation uses a variant of the flagging technique described in [18], except that it can flag two different nodes with the same operation record.

Oshman and Shavit [33] introduce a randomized data structure called a skip trie. It combines an x-fast trie with a truncated skip list whose max height is log2log2u\log_{2}\log_{2}u (i.e. it is a y-fast trie whose balanced binary search trees are replaced with a truncated skip list). Only keys that are in the top level of the skip list are in the x-fast trie. They give a lock-free implementation of a skip trie supporting Search, Insert, Delete, and Predecessor operations with O(c~(op)+loglogu)O(\tilde{c}(op)+\log\log u) expected amortized step complexity from registers, CAS and DCSS. Their x-fast trie uses lock-free hash tables [39]. Their x-fast trie implementation supports update operations with O(c˙(op)logu)O(\dot{c}(op)\cdot\log u) expected amortized step complexity. Their skip list implementation is similar to Fomitchev and Ruppert’s skip list implementation [21]. In the worst-case (for example, when the height of the skip list is 0), a skip trie performs the same as a linked list, so Search and Predecessor take Θ(n)\Theta(n) steps, even when there are no concurrent updates. Our lock-free binary trie implementation is deterministic, does not rely on hashing, uses primitives supported in hardware, and always performs well when there are no concurrent update operations. Furthermore, Search operations in our binary trie complete in a constant number of reads in the worst-case.

The first universal constructions were by Herhily [26, 27]. To achieve wait-freedom, he introduced an announcement array where operations announce themselves. Processes help perform these announced operations in a round-robin order. Barnes [2] gives a universal construction for obtaining lock-free data structures. He introduces the idea of using operation records to facilitate helping.

Lock-free data structures can be augmented to support iterators, snapshots, and range queries [13, 20, 34, 35]. Wei et al. [43] give a simple technique to take snapshots of concurrent data structures in constant time. This is done by implementing a versioned CAS object that allows old values of the object to be read. The number of steps needed to read the value of a versioned CAS object at the time of a snapshot is equal to the number of times its value changed since the snapshot was taken. Provided update operations only perform a constant amortized number of successful versioned CAS operations, balanced binary search trees can be augmented to support Predecessor with O(c¯(op)+logn(op))=O(c˙(op)+logn(op))O(\bar{c}(op)+\log n(op))=O(\dot{c}(op)+\log n(op)) amortized step complexity.

4 Relaxed Binary Trie

In this section, we describe our relaxed binary trie, which is used as a component of our lock-free binary trie. We begin by giving the formal specification of the relaxed binary trie in Section 4.1. In Section 4.2, we describe how our implementation is represented in memory. In Section 4.3, we give a high-level description of our algorithms for each operation. In Section 4.4, we give a detailed description of our algorithms for each operation and its pseudocode. Finally, in Section 4.5, we show that our implementation satisfies the specification.

4.1 Specification

A relaxed binary trie is a concurrent data structure maintaining a dynamic set SS from the universe U={0,,u1}U=\{0,\dots,u-1\} that supports the following strongly linearizable operations:

  • TrieInsert(x)(x), which adds key xx into SS if it is not already in SS,

  • TrieDelete(x)(x), which removes key xx from SS if it is in SS, and

  • TrieSearch(x)(x), which returns True if key xSx\in S, and False otherwise.

It additionally supports the (non-linearizable) RelaxedPredecessor(y)(y) operation. Its concurrent specification relies on a few definitions.

Because the relaxed binary trie is strongly linearizable with respect to all of its update operations, it is possible to determine the value of the set SS represented by the data structure in every configuration of every execution from the sequence of linearization points of the update operations prior to this configuration. For any execution of the relaxed binary trie and any key xUx\in U, consider the sequence σ\sigma of TrieInsert(x)(x) and TrieDelete(x)(x) operations in the order of their linearization points. A TrieInsert(x)(x) operation is SS-modifying if it is the first TrieInsert(x)(x) operation in σ\sigma, or if it is the first TrieInsert(x)(x) operation that follows a TrieDelete(x)(x) operation. In other words, a TrieInsert(x)(x) operation is SS-modifying if it successfully adds the key xx to SS. Likewise, a TrieDelete(x)(x) operation is SS-modifying if it is the first TrieDelete(x)(x) operation that follows a TrieInsert(x)(x) operation. A key xx is completely present throughout a RelaxedPredecessor operation, pOppOp, if there is an SS-modifying TrieInsert(x)(x) operation, iOpiOp, that completes before the invocation of pOppOp and there is no SS-modifying TrieDelete(x)(x) operation that is linearized after iOpiOp but before the end of pOppOp.

Specification of RelaxedPredecessor: Let pOppOp be a completed RelaxedPredecessor(y)(y) operation in some execution. Let kk be the largest key less than yy that is completely present throughout pOppOp, or 1-1 if no such key exists. Then pOppOp returns a value in {}{k,,y1}\{\bot\}\cup\{k,\dots,y-1\} such that:

  • If pOppOp returns \bot, then there exists a key xx, where k<x<yk<x<y, such that the last SS-modifying update operation linearized prior to the end of pOppOp is concurrent with pOppOp.

  • If pOppOp returns a key x>kx>k, then xSx\in S sometime during pOppOp.

These properties imply that if, for all k<x<yk<x<y, the SS-modifying update operation with key xx that was last linearized prior to the end of pOppOp is not concurrent with pOppOp, then pOppOp returns kk. In this case, if the last SS-modifying update operation with key xx linearized prior to the end of pOppOp is a TrieInsert(x)(x) operation, then it was completed before the start of pOppOp. But, then xx is completely present throughout pOppOp, contradicting the definition of kk. Therefore, kk is the predecessor of yy throughout pOppOp.

4.2 Our Relaxed Binary Trie Implementation

Our wait-free implementation of a relaxed binary trie supports TrieSearch with O(1)O(1) worst-case step complexity, and TrieInsert, TrieDelete, and RelaxedPredecessor with O(logu)O(\log u) worst-case step complexity. We first describe the major components of the relaxed binary trie and how it is stored in memory.

Like the sequential binary trie, the relaxed binary trie consists of a collection of arrays, Di\mathit{D}_{i} for 0ib=log2u0\leq i\leq b=\lceil\log_{2}u\rceil. Each array Di\mathit{D}_{i}, for 0ib0\leq i\leq b, has length 2i2^{i} and represents the nodes at depth ii of the relaxed binary trie.

An update node is created by a TrieInsert or TrieDelete operation. It is an INS node if it is created by a TrieInsert operation, or a DEL node if it is created by a TrieDelete operation. It includes the input key of the operation that created it.

There is an array latest indexed by each key in UU, where latest[x]\textit{latest}[x] contains a pointer to an update node with key xUx\in U. The update node pointed to by latest[x]\textit{latest}[x] belongs to the last SS-modifying TrieInsert(x)(x) and TrieDelete(x)(x) operation that has been linearized. So the update node pointed to by latest[x]\textit{latest}[x] is an INS node if and only if xSx\in S. In the initial configuration, when S=S=\emptyset, latest[x]\textit{latest}[x] points to a dummy DEL node.

Recall that, in the sequential binary trie, each binary trie node tt contains the bit 1 if there is leaf in its subtrie whose key is in SS, and 0 otherwise. These bits need to be accurately set for the correctness of predecessor operations. In our relaxed binary trie, each binary trie node has an associated value, called its interpreted bit. The interpreted bit of a leaf with key xx is 1 if and only if xSx\in S.

For each internal binary trie node tt, let UtU_{t} be the set of keys of the leaves contained in the subtrie rooted at tt. When there are no active update operations with keys in UtU_{t}, the interpreted bit of tt is the logical OR of the interpreted bits of the leaves of the subtrie rooted at tt. More generally, our relaxed binary trie maintains the following two properties concerning its interpreted bits. For all binary trie nodes tt and configurations CC:

  1. IB0

    If UtS=U_{t}\cap S=\emptyset and for all xUtx\in U_{t}, either there has been no SS-modifying TrieDelete(x)(x) operation or the last SS-modifying TrieDelete(x)(x) operation linearized prior to CC is no longer active, then the interpreted bit of tt is 0 in CC.

  2. IB1

    If there exists xUtSx\in U_{t}\cap S such that the last SS-modifying TrieInsert(x)(x) operation linearized prior to CC is no longer active, then the interpreted bit of tt is 1 in CC.

When there are active update operations with a key in UtU_{t}, the interpreted bit of the binary trie node tt may be different from the bit stored in tt of the sequential binary trie representing the same set.

The interpreted bit of tt is not physically stored in tt, but is, instead, computed from the update node pointed to by latest[x]\textit{latest}[x], for some key xUtx\in U_{t}. Each internal binary trie node tt stores this key xx. The interpreted bit of tt depends on the update node, 𝑢𝑁𝑜𝑑𝑒\mathit{uNode}, pointed to by latest[x]\textit{latest}[x]. If 𝑢𝑁𝑜𝑑𝑒\mathit{uNode} is an INS node, the interpreted bit of tt is 1. When 𝑢𝑁𝑜𝑑𝑒\mathit{uNode} is a DEL node, the interpreted bit of tt is determined by two thresholds, 𝑢𝑁𝑜𝑑𝑒.upper0Boundary\mathit{uNode}.\mathit{upper0Boundary} and 𝑢𝑁𝑜𝑑𝑒.lower1Boundary\mathit{uNode}.\mathit{lower1Boundary}. In this case, the interpreted bit of tt is

  • 1 if t.0pt𝑢𝑁𝑜𝑑𝑒.lower1Boundaryt.0pt\geq\mathit{uNode}.\mathit{lower1Boundary},

  • 0 if t.0pt<𝑢𝑁𝑜𝑑𝑒.lower1Boundaryt.0pt<\mathit{uNode}.\mathit{lower1Boundary} and t.0pt𝑢𝑁𝑜𝑑𝑒.upper0Boundaryt.0pt\leq\mathit{uNode}.\mathit{upper0Boundary}, and

  • 1 otherwise.

Only TrieInsert operations modify lower1Boundary\mathit{lower1Boundary} and only TrieDelete operations modify upper0Boundary\mathit{upper0Boundary}. We discuss these thresholds in more detail when describing TrieInsert and TrieDelete in the following section.

4.3 High-Level Algorithm Description

A TrieSearch(x)(x) operation reads the update node pointed to by latest[x]\textit{latest}[x], returns True if it is an INS node, and returns False if it is a DEL node.

An TrieInsert(x)(x) or TrieDelete(x)(x) operation, opop, begins by finding the first activated update node in latest[x]\textit{latest}[x]. If it has the same type as opop, then opop can return because SS does not need to be modified. Otherwise opop creates a new inactive update node 𝑢𝑁𝑜𝑑𝑒\mathit{uNode} with key xx. It then attempts to add 𝑢𝑁𝑜𝑑𝑒\mathit{uNode} to the beginning of latest[x]\textit{latest}[x]. It then attempts to change latest[x]\textit{latest}[x] to point to 𝑢𝑁𝑜𝑑𝑒\mathit{uNode} using CAS. If successful, the operation is linearized at this step. Any other update nodes in latest[x]\textit{latest}[x] are then removed by setting the next pointer of 𝑢𝑁𝑜𝑑𝑒\mathit{uNode} to \bot. If multiple update operations with key xx concurrently attempt to add an update node to the beginning of latest[x]\textit{latest}[x], exactly one will succeed. Update operations that are unsuccessful instead help the update operation that succeeded by setting the status of its update node to active.

In the case that uOpuOp successfully changes latest[x]\textit{latest}[x] to point to 𝑢𝑁𝑜𝑑𝑒\mathit{uNode}, uOpuOp must then update the interpreted bits of the relaxed binary trie. Both TrieInsert and TrieDelete operations update the interpreted bits of the relaxed binary trie in manners similar to the sequential data structure. A TrieInsert(x)(x) operation traverses from the leaf with key xx to the root and sets the interpreted bits along this path to 1 if they are not already 1. This is described in more detail in Section 4.3.1. A TrieDelete(x)(x) operation traverses the binary trie starting from the leaf with key xx and proceeds to the root. It changes the interpreted bit of a binary trie node on this path to 0 if both its children have interpreted bit 0, and returns otherwise. This is described in more detail in Section 4.3.2.

4.3.1 TrieInsert

Consider a latest TrieDelete(x)(x) operation, iOpiOp, and let 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} be the INS node it created, so 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} is the first activated update node in latest[x]\textit{latest}[x]. Let tt be a binary trie node iOpiOp encounters as it is updating the interpreted bits of the binary trie. If tt already has interpreted bit 1, then it does not need to be updated. This can happen when the interpreted bit of tt depends on an INS node (for example, when tt stores key xx and hence depends on 𝑖𝑁𝑜𝑑𝑒\mathit{iNode}). So suppose the interpreted bit of tt is 0. In this case, tt stores key xxx^{\prime}\neq x and its interpreted bit depends on a DEL node 𝑑𝑁𝑜𝑑𝑒\mathit{dNode}. Only a Delete operation can change the key stored in tt and it can only change the key to its own key. We do not allow Insert operations to change this key to avoid concurrent Delete operations from repeatedly interfering with an Insert operation. Instead, Insert operations can modify 𝑑𝑁𝑜𝑑𝑒.lower1Boundary\mathit{dNode}.\mathit{lower1Boundary} to change the interpreted bit of tt from 0 to 1. This is a min-register whose value is initially b+1b+1, which is greater than the height of any binary trie node. All binary trie nodes that depend on 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} and whose height is at least the value of 𝑑𝑁𝑜𝑑𝑒.lower1Boundary\mathit{dNode}.\mathit{lower1Boundary} have interpreted bit 1. Therefore, to change the interpreted bit of tt from 0 to 1, iOpiOp can perform MinWrite(t.0pt)(t.0pt) to 𝑑𝑁𝑜𝑑𝑒.lower1Boundary\mathit{dNode}.\mathit{lower1Boundary}. This also changes the interpreted bit of all ancestors of tt that depend on 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} to 1. A min-register is used so that modifying 𝑑𝑁𝑜𝑑𝑒.lower1Boundary\mathit{dNode}.\mathit{lower1Boundary} never changes the interpreted bit of any binary trie node from 1 to 0.

An example execution of an Insert operation updating the interpreted bits of the relaxed binary trie is shown in Figure 2. Blue rectangles represent active INS nodes, while red rectangles represent active DEL nodes. The number in each binary trie node is its interpreted bit. The dashed arrow from an internal binary trie node points to the update node it depends on. Note that the dashed arrow is not a physical pointer stored in the binary trie node. Under each update node are the values of its lower1Boundary\mathit{lower1Boundary}, abbreviated l1bl1b, and upper0Boundary\mathit{upper0Boundary}, abbreviated u0bu0b. Figure 2(a) shows a possible state of the data structure where S=S=\emptyset. In Figure 2(b), an Insert(0)(0) operation, iOpiOp, activates its newly added INS node in latest[0]\textit{latest}[0]. This simultaneously changes the interpreted bit of the leaf with key 0 and its parent from 0 to 1 in a single step. In Figure 2(c), iOpiOp changes the interpreted bit of the root from 0 to 1. This is done using a MinWrite, which changes the lower1Boundary\mathit{lower1Boundary} of the DEL node in latest[3]\textit{latest}[3] (i.e. the update node that the root depends on) from 3 to the height of the root.

Refer to caption
(a)
Refer to caption
(b)
Refer to caption
(c)
Figure 2: An example of an Insert(0)(0) operation setting the interpreted bits of the binary trie nodes from its leaf with key 0 to the root to 1.

4.3.2 TrieDelete

Consider a latest Delete(x)(x) operation, dOpdOp, and let 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} be the DEL node it created, so 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} is the first activated update node in latest[x]\textit{latest}[x]. Furthermore, this means that the leaf with key xx has interpreted bit 0.

Let tt be an internal binary trie node on the path from the leaf with key xx to the root. Suppose dOpdOp successfully changed the interpreted bit of one of tt’s children to 0. If the interpreted bit of the other child of tt is 0, then dOpdOp attempts to change the interpreted bit of tt to 0. First, dOpdOp tries to change the update node that tt depends on to 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} by changing the key stored in tt to xx. After a constant number of reads and at most 2 CAS operations, our algorithm guarantees that if dOpdOp does not successfully change tt to depend on 𝑑𝑁𝑜𝑑𝑒\mathit{dNode}, then for some yUty\in U_{t}, a latest Delete(y)(y) operation, dOpdOp^{\prime}, changed tt to depend on the DEL node 𝑑𝑁𝑜𝑑𝑒\mathit{dNode}^{\prime} created by dOpdOp^{\prime}. In this case, dOpdOp^{\prime} will change the interpreted bit of tt to 0 on dOpdOp’s behalf, so dOpdOp can stop updating the interpreted bits of the binary trie. Suppose dOpdOp does successfully change tt to depend on 𝑑𝑁𝑜𝑑𝑒\mathit{dNode}. To change the interpreted bit of 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} to 0, dOpdOp writes t.0ptt.0pt into 𝑑𝑁𝑜𝑑𝑒.upper0Boundary\mathit{dNode}.\mathit{upper0Boundary}, which is a register with initial value 0. This indicates that all binary trie nodes at height tt and below that depend on 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} have interpreted bit 0. Only dOpdOp, the creator of 𝑑𝑁𝑜𝑑𝑒\mathit{dNode}, writes to 𝑑𝑁𝑜𝑑𝑒.upper0Boundary\mathit{dNode}.\mathit{upper0Boundary}. Since dOpdOp changes the interpreted bits of binary trie nodes in order from the leaf with key xx up to the root, 𝑑𝑁𝑜𝑑𝑒.upper0Boundary\mathit{dNode}.\mathit{upper0Boundary} is only ever incremented by 1 starting from 0.

An example execution of Delete operations updating the interpreted bits of the relaxed binary trie is shown in Figure 3. Figure 3(a) shows a possible state of the data structure where S={0,1}S=\{0,1\}. In Figure 3(b), a Delete(0)(0), dOpdOp, and a Delete(1)(1), dOpdOp^{\prime}, activate their newly added DEL nodes. This removes the keys 0 and 1 from SS. The leaves with keys 0 and 1 have interpreted bit 0. In Figure 3(c), dOpdOp^{\prime} sees that its sibling leaf has interpreted bit 0. Then dOpdOp^{\prime} successfully changes the left child of the root to depend on its DEL node, while dOpdOp is unsuccessful and returns. In Figure 3(d), dOpdOp^{\prime} increments the upper0Boundary\mathit{upper0Boundary} of its DEL node, so it is now equal to the height of the left child of the root. This changes the interpreted bit of the left child of the root to 0. In Figure 3(e), dOpdOp^{\prime} sees that the right child of the root has interpreted bit 0, so the interpreted bit of the root needs to be updated. So dOpdOp^{\prime} changes the root to depend on its DEL node. In Figure 3(f), dOpdOp^{\prime} increments the upper0Boundary\mathit{upper0Boundary} of its DEL node, so it is now equal to the height of the root. This changes the interpreted bit of the left child of the root to 0.

Refer to caption
(a)
Refer to caption
(b)
Refer to caption
(c)
Refer to caption
(d)
Refer to caption
(e)
Refer to caption
(f)
Figure 3: An example of a Delete(0)(0) and Delete(1)(1) updating the interpreted bits of the binary trie.

4.3.3 RelaxedPredecessor

A RelaxedPredecessor(y)(y) operation, pOppOp, traverses the relaxed binary trie in a manner similar to the sequential algorithm, except that it uses the interpreted bits of binary trie nodes to direct the traversal. If pOppOp completes its traversal of the relaxed binary trie following the sequential algorithm, then it either returns the key of the leaf it reaches, or 1-1 if the traversal ended at the root.

It is possible that pOppOp is unable to complete a traversal of the relaxed binary trie due to inaccurate interpreted bits. During the downward part of its traversal, it may encounter an internal binary trie node with interpreted bit 1, but both of its children have interpreted bit 0. When this occurs, pOppOp terminates and returns \bot. There is a concurrent update operation that needs to update this part of the relaxed binary trie.

4.4 Detailed Algorithm Description and Pseudocode

In this section we give a detailed description of the algorithm for each relaxed binary trie operation, and present its pseudocode.

4.4.1 TrieSearch and Basic Helper Functions

The TrieSearch(x)(x) algorithm finds the update node pointed to by latest[x]\textit{latest}[x]. It returns True if this update node has type INS, and False if this update node has type DEL.

We use the helper function FindLatest(x)(x) to return the update node pointed to by latest[x]\textit{latest}[x]. The helper function FirstActivated(v)(v) takes a pointer vv to an update node and checks if vv is the update node pointed to by latest[v.key]\textit{latest}[v.key]. The implementation of these helper functions are simple the case of the relaxed binary trie, but will be replaced with a different implementation when we consider the lock-free binary trie.

The helper function InterpretedBit(t)(t) receives a binary trie node tt, and returns its interpreted bit. Its implementation follows from the definition of the interpreted bit.

1:Algorithm FindLatest(x)(x)
2:     return latest[x]\ell\leftarrow\textit{latest}[x]
3:Algorithm TrieSearch(x)(x)
4:     𝑢𝑁𝑜𝑑𝑒FindLatest(x)\mathit{uNode}\leftarrow\textsc{FindLatest}(x)
5:     if 𝑢𝑁𝑜𝑑𝑒.𝑡𝑦𝑝𝑒=INS\mathit{uNode}.\mathit{type}=\textsc{INS} then return True
6:     else return False      
7:Algorithm FirstActivated(v)(v)
8:     latest[v.𝑘𝑒𝑦]\ell\leftarrow\textit{latest}[v.\mathit{key}]
9:     return v=v=\ell
10:Algorithm InterpretedBit(t)(t)
11:     𝑢𝑁𝑜𝑑𝑒FindLatest(t.𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟.𝑘𝑒𝑦)\mathit{uNode}\leftarrow\textsc{FindLatest}(t.\mathit{dNodePtr}.\mathit{key})
12:     if 𝑢𝑁𝑜𝑑𝑒.𝑡𝑦𝑝𝑒=INS\mathit{uNode}.\mathit{type}=\text{INS}  then return 11      
13:     if t.0pt𝑢𝑁𝑜𝑑𝑒.lower1Boundaryt.0pt\geq\mathit{uNode}.\mathit{lower1Boundary} then return 11      
14:     if t.0pt𝑢𝑁𝑜𝑑𝑒.upper0Boundaryt.0pt\leq\mathit{uNode}.\mathit{upper0Boundary} then return 0      
15:     return 11

4.4.2 TrieInsert

A TrieInsert(x)(x) operation iOpiOp begins by reading the update node, 𝑑𝑁𝑜𝑑𝑒\mathit{dNode}, pointed to by latest[x]\textit{latest}[x]. If 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} is not a DEL node, then xx is already in SS so TrieInsert(x)(x) returns. Otherwise iOpiOp creates a new INS node, denoted 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} with key xx. It then attempts to change latest[x]\textit{latest}[x] to point to 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} using CAS (on line 22). A TrieInsert(x)(x) operation that successfully performs this CAS adds xx to SS, and is linearized at this successful CAS. If multiple TrieInsert(x)(x) operations concurrently attempt to change latest[x]\textit{latest}[x], exactly one will succeed. Any TrieInsert(x)(x) operations that are unsuccessful can return, because some other TrieInsert(x)(x) operation successfully added xx to SS. Note that by updating latest[x]\textit{latest}[x] to point to 𝑖𝑁𝑜𝑑𝑒\mathit{iNode}, the interpreted bit of the leaf with key xx is 1.

16:Algorithm TrieInsert(x)(x)
17:     𝑑𝑁𝑜𝑑𝑒FindLatest(x)\mathit{dNode}\leftarrow\textsc{FindLatest}(x)
18:     if 𝑑𝑁𝑜𝑑𝑒.𝑡𝑦𝑝𝑒DEL\mathit{dNode}.\mathit{type}\neq\text{DEL} then return \triangleright xx is already in SS      
19:     Let 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} be a pointer to a new update node:
20:      𝑖𝑁𝑜𝑑𝑒.𝑘𝑒𝑦x\mathit{iNode}.\mathit{key}\leftarrow x
21:      𝑖𝑁𝑜𝑑𝑒.𝑡𝑦𝑝𝑒INS\mathit{iNode}.\mathit{type}\leftarrow\text{INS}
22:     if CAS(latest[x].ℎ𝑒𝑎𝑑,𝑑𝑁𝑜𝑑𝑒,𝑖𝑁𝑜𝑑𝑒)=False(\textit{latest}[x].\mathit{head},\mathit{dNode},\mathit{iNode})=\textsc{False} then\triangleright Insert operation is linearized
23:         return      
24:     InsertBinaryTrie(𝑖𝑁𝑜𝑑𝑒)(\mathit{iNode})
25:     return

The algorithm to update the binary trie is described in InsertBinaryTrie(𝑖𝑁𝑜𝑑𝑒)(\mathit{iNode}), where 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} is the INS node created by iOpiOp. The purpose of InsertBinaryTrie is to set the interpreted bit of each binary trie node tt on the path from the parent of the leaf with key xx to the root to 1. The algorithm first determines the current interpreted bit of tt on lines 28 to 30 by reading fields of the first activated update node, 𝑢𝑁𝑜𝑑𝑒\mathit{uNode}, in latest[x]\textit{latest}[x]. If the interpreted bit of tt is 1, opop proceeds to the parent of tt. If the interpreted bit of tt is 0, then 𝑢𝑁𝑜𝑑𝑒.lower1Boundary\mathit{uNode}.\mathit{lower1Boundary} is updated to the value t.0ptt.0pt using a MinWrite on line 33. Updating 𝑢𝑁𝑜𝑑𝑒.lower1Boundary\mathit{uNode}.\mathit{lower1Boundary} serves the purpose of changing the interpreted bit of tt to 1, as well as informing the Delete operation that created 𝑢𝑁𝑜𝑑𝑒\mathit{uNode} to stop updating the binary trie. It is problematic if iOpiOp crashes if it is poised to perform this MinWrite. So opop sets 𝑖𝑁𝑜𝑑𝑒.target\mathit{iNode}.\textit{target} to point to 𝑢𝑁𝑜𝑑𝑒\mathit{uNode} beforehand, indicating iOpiOp wishes to perform a MinWrite to 𝑢𝑁𝑜𝑑𝑒\mathit{uNode}. The field 𝑖𝑁𝑜𝑑𝑒.target\mathit{iNode}.\textit{target} is read by TrieDelete(x)(x) operations to help iOpiOp in case it crashes.

26:Algorithm InsertBinaryTrie(𝑖𝑁𝑜𝑑𝑒)(\mathit{iNode})
27:     for  each binary trie node tt on path from parent of the leaf 𝑖𝑁𝑜𝑑𝑒.𝑘𝑒𝑦\mathit{iNode}.\mathit{key} to root  do
28:         𝑢𝑁𝑜𝑑𝑒FindLatest(t.𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟.𝑘𝑒𝑦)\mathit{uNode}\leftarrow\textsc{FindLatest}(t.\mathit{dNodePtr}.\mathit{key})
29:         if 𝑢𝑁𝑜𝑑𝑒.type=DEL\mathit{uNode}.type=\text{DEL} then
30:              if t.0pt<𝑢𝑁𝑜𝑑𝑒.lower1Boundaryt.0pt<\mathit{uNode}.\mathit{lower1Boundary} and t.0pt𝑢𝑁𝑜𝑑𝑒.upper0Boundaryt.0pt\leq\mathit{uNode}.\mathit{upper0Boundary} then
31:                  𝑖𝑁𝑜𝑑𝑒.target𝑢𝑁𝑜𝑑𝑒\mathit{iNode}.\textit{target}\leftarrow\mathit{uNode}
32:                  if FirstActivated(𝑖𝑁𝑜𝑑𝑒)=False(\mathit{iNode})=\textsc{False} then return                   
33:                  MinWrite(𝑢𝑁𝑜𝑑𝑒.lower1Boundary,t.0pt)\textsc{MinWrite}(\mathit{uNode}.\mathit{lower1Boundary},t.0pt)                             

4.4.3 TrieDelete

A TrieDelete(x)(x) operation dOpdOp checks if the update node pointed to by latest[x]\textit{latest}[x] is an INS node. If not, it returns because xx is not in SS. Otherwise, it creates a new DEL node, 𝑑𝑁𝑜𝑑𝑒\mathit{dNode}. It then updates latest[x]\textit{latest}[x] to point to 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} using CAS in the same way as TrieInsert(x)(x). A TrieDelete(x)(x) that successfully performs this CAS removes xx from SS, and is linearized at this successful CAS.

34:Algorithm TrieDelete(x)(x)
35:     𝑖𝑁𝑜𝑑𝑒FindLatest(x)\mathit{iNode}\leftarrow\textsc{FindLatest}(x)
36:     if 𝑖𝑁𝑜𝑑𝑒.𝑡𝑦𝑝𝑒INS\mathit{iNode}.\mathit{type}\neq\text{INS} then return \triangleright xx is not in SS      
37:     Let 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} be a pointer to a new update node:
38:      𝑑𝑁𝑜𝑑𝑒.𝑘𝑒𝑦x\mathit{dNode}.\mathit{key}\leftarrow x
39:      𝑑𝑁𝑜𝑑𝑒.𝑡𝑦𝑝𝑒DEL\mathit{dNode}.\mathit{type}\leftarrow\text{DEL}
40:     if CAS(latest[x],𝑖𝑁𝑜𝑑𝑒,𝑑𝑁𝑜𝑑𝑒)=False(\textit{latest}[x],\mathit{iNode},\mathit{dNode})=\textsc{False} then\triangleright Delete operation is linearized
41:         return      
42:     𝑖𝑁𝑜𝑑𝑒.target.stopTrue\mathit{iNode}.\textit{target}.\textit{stop}\leftarrow\textsc{True}
43:     DeleteBinaryTrie(𝑑𝑁𝑜𝑑𝑒)(\mathit{dNode})
44:     return

The operation dOpdOp then calls DeleteBinaryTrie(𝑑𝑁𝑜𝑑𝑒)(\mathit{dNode}) to update the interpreted bits of the relaxed binary trie nodes from the parent of the leaf with key xx to the root. Let tt be an internal binary trie node on the path from the leaf with key xx to the root. Suppose dOpdOp successfully changed the interpreted bit of one of tt’s children to 0. If the interpreted bit of the other child of tt is 0, then dOpdOp attempts to change the interpreted bit of tt to 0. Recall that tt depends on the first activated update node in latest[t.𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟.𝑘𝑒𝑦]\textit{latest}[t.\mathit{dNodePtr}.\mathit{key}]. To change tt to depend on 𝑑𝑁𝑜𝑑𝑒\mathit{dNode}, dOpdOp performs CAS to attempt to change t.𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟t.\mathit{dNodePtr} to point to 𝑑𝑁𝑜𝑑𝑒\mathit{dNode}. Note that dOpdOp performs two attempts of this CAS, each time checking its 𝑑𝑁𝑜𝑑𝑒.stop\mathit{dNode}.\textit{stop} is not set to True (indicating a concurrent Insert(x)(x) wants to set the interpreted bit of tt to 1) and that 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} is still the first activated update node in latest[x]\textit{latest}[x]. Two CAS attempts are performed to prevent out-dated Delete operations that were poised to perform CAS from conflicting with latest Delete operations. If dOpdOp is unsuccessful in both its CAS attempts, it can stop updating the binary trie because some concurrent Delete(x)(x^{\prime}) operation, with key xUtx^{\prime}\in U_{t}, successfully changed t.𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟t.\mathit{dNodePtr} to point to its own DEL node. Otherwise opop is successful in changing the interpreted bit of tt to depend on 𝑑𝑁𝑜𝑑𝑒\mathit{dNode}. Immediately after opop’s successful CAS, the interpreted bit of tt is still 1 (because 𝑑𝑁𝑜𝑑𝑒.upper0Boundary\mathit{dNode}.\mathit{upper0Boundary} has not yet been incremented to t.0ptt.0pt). Once again, opop verifies both children of tt have interpreted bit 0, otherwise it returns. To change the interpreted bit of 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} to 0, dOpdOp writes t.0ptt.0pt into 𝑑𝑁𝑜𝑑𝑒.upper0Boundary\mathit{dNode}.\mathit{upper0Boundary}, which increments its value. This indicates that all binary trie nodes at height tt and below that depend on 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} have interpreted bit 0. Only dOpdOp, the creator of 𝑑𝑁𝑜𝑑𝑒\mathit{dNode}, writes to 𝑑𝑁𝑜𝑑𝑒.upper0Boundary\mathit{dNode}.\mathit{upper0Boundary}. Since dOpdOp changes the interpreted bits of binary trie nodes in order from the leaf with key xx to the root, 𝑑𝑁𝑜𝑑𝑒.upper0Boundary\mathit{dNode}.\mathit{upper0Boundary} is only ever incremented by 1 starting from 0.

45:Algorithm DeleteBinaryTrie(𝑑𝑁𝑜𝑑𝑒)(\mathit{dNode})
46:     tt\leftarrow leaf of binary trie with key 𝑑𝑁𝑜𝑑𝑒.key\mathit{dNode}.key
47:     while tt is not the root of the binary trie do
48:         if  InterpretedBit(t.𝑠𝑖𝑏𝑙𝑖𝑛𝑔)=1\textsc{InterpretedBit}(t.\mathit{sibling})=1 or InterpretedBit(t)=1\textsc{InterpretedBit}(t)=1 then return          
49:         tt.parentt\leftarrow t.parent
50:         dt.𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟\mathit{d}\leftarrow t.\mathit{dNodePtr}
51:         if FirstActivated(𝑑𝑁𝑜𝑑𝑒)(\mathit{dNode}) = False then return          
52:         if 𝑑𝑁𝑜𝑑𝑒.stop=True\mathit{dNode}.\textit{stop}=\textsc{True} or 𝑑𝑁𝑜𝑑𝑒.lower1Boundaryb+1\mathit{dNode}.\mathit{lower1Boundary}\neq b+1 then return          
53:         if  CAS(t.𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟,d,𝑑𝑁𝑜𝑑𝑒)=False(t.\mathit{dNodePtr},d,\mathit{dNode})=\textsc{False} then
54:              dt.𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟\mathit{d}\leftarrow t.\mathit{dNodePtr}
55:              if FirstActivated(𝑑𝑁𝑜𝑑𝑒)(\mathit{dNode}) = False then return               
56:              if 𝑑𝑁𝑜𝑑𝑒.stop=True\mathit{dNode}.\textit{stop}=\textsc{True} or 𝑑𝑁𝑜𝑑𝑒.lower1Boundaryb+1\mathit{dNode}.\mathit{lower1Boundary}\neq b+1 then return               
57:              if CAS(t.𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟,d,𝑑𝑁𝑜𝑑𝑒)=False(t.\mathit{dNodePtr},d,\mathit{dNode})=\textsc{False} then return                        
58:         if InterpretedBit(t.𝑙𝑒𝑓𝑡)=1\textsc{InterpretedBit}(t.\mathit{left})=1 or InterpretedBit(t.𝑟𝑖𝑔ℎ𝑡)=1\textsc{InterpretedBit}(t.\mathit{right})=1 then return          
59:         𝑑𝑁𝑜𝑑𝑒.upper0Boundaryt.0pt\mathit{dNode}.\mathit{upper0Boundary}\leftarrow t.0pt      

4.4.4 RelaxedPredecessor

A RelaxedPredecessor(y)(y) operation, pOppOp, begins by traversing up the relaxed binary trie starting from the leaf with key yy towards the root (during the while-loop on line62). If the left child of each node on this path either has interpreted bit 0 or is also on this path, then pOppOp returns 1-1 (on line 65). Otherwise, consider the first node on this path whose left child tt (set on line 67) has interpreted bit 1 and is not on this path. Starting from tt, pOppOp traverses the right-most path of binary trie nodes with interpreted bit 1 (during the while-loop on line68). If a binary trie node tt is encountered where both its children have interpreted bit 0, then \bot is returned (on line 65. Otherwise, the pOppOp reaches a leaf, and returns its key (on line 77).

60:Algorithm RelaxedPredecessor(y)(y)
61:     tt\leftarrow the binary trie node represented by Db[y]\mathit{D}_{b}[y]
62:     while tt is the left child of t.𝑝𝑎𝑟𝑒𝑛𝑡t.\mathit{parent} or InterpretedBit(t.𝑠𝑖𝑏𝑙𝑖𝑛𝑔)=0\textsc{InterpretedBit}(t.\mathit{sibling})=0 do
63:         tt.parentt\leftarrow t.parent
64:         if tt is the root then
65:              return 1-1               
66:     \triangleright Traverse right-most path of nodes with interpreted bit 1 from t.𝑝𝑎𝑟𝑒𝑛𝑡.𝑙𝑒𝑓𝑡t.\mathit{parent}.\mathit{left}
67:     tt.𝑝𝑎𝑟𝑒𝑛𝑡.𝑙𝑒𝑓𝑡t\leftarrow t.\mathit{parent}.\mathit{left}
68:     while t.ℎ𝑒𝑖𝑔ℎ𝑡>0t.\mathit{height}>0  do
69:         if InterpretedBit(t.𝑟𝑖𝑔ℎ𝑡)=1\textsc{InterpretedBit}(t.\mathit{right})=1 then
70:              tt.𝑟𝑖𝑔ℎ𝑡t\leftarrow t.\mathit{right}
71:         else if InterpretedBit(t.𝑙𝑒𝑓𝑡)=1\textsc{InterpretedBit}(t.\mathit{left})=1 then
72:              tt.𝑙𝑒𝑓𝑡t\leftarrow t.\mathit{left}
73:         else
74:              \triangleright both children of tt have interpreted bit 0
75:              return \bot               
76:     \triangleright tt is a leaf node with key t.keyt.key
77:     return t.𝑘𝑒𝑦t.\mathit{key}

4.5 Proof of Correctness

In this section, we prove that our relaxed binary trie implementation is linearizable. The proof is organized as follows. In Section 4.5.1 we give the linearization points of TrieSearch, TrieInsert, and TrieDelete, and prove that the implementation is strongly linearizable with respect to these operation types. In Section 4.5.2 we prove the properties satisfied by the interpreted bits of the binary trie. In Section 5.3 we prove that RelaxedPredecessor operations follow the specification of the relaxed binary trie.

4.5.1 Strong Linearizability of TrieInsert, TrieDelete, and TrieSearch

A TrieSearch(x)(x) operation is linearized immediately after it reads latest[x]\textit{latest}[x]. We argue that this operation returns True if and only if xSx\in S in this configuration.

Lemma 4.1.

Let opop be a TrieSearch(x)(x) operation. Then opop returns True if and only if in the configuration CC immediately after opop reads latest[x]\textit{latest}[x], xSx\in S.

Proof.

Let 𝑢𝑁𝑜𝑑𝑒\mathit{uNode} be the update node pointed to by latest[x]\textit{latest}[x] read by opop, and let CC be the configuration immediately after this read by opop. If opop returned True, it read that 𝑢𝑁𝑜𝑑𝑒.𝑡𝑦𝑝𝑒=INS\mathit{uNode}.\mathit{type}=\textsc{INS}. The type of an update node is immutable, so latest[x]\textit{latest}[x] points to an INS node in CC. So it follows by definition that xSx\in S in CC. If opop returned False, it read that 𝑢𝑁𝑜𝑑𝑒.𝑡𝑦𝑝𝑒=DEL\mathit{uNode}.\mathit{type}=\textsc{DEL}. It follows by definition that xSx\notin S in CC. ∎

The TrieInsert(x)(x) and TrieDelete(x)(x) operations that are SS-modifying successfully change latest[x]\textit{latest}[x] to point to their own update node using CAS, and are linearized at this CAS step. A TrieInsert(x)(x) operation that is not SS-modifying does not update latest[x]\textit{latest}[x] to point to its own update node. This happens when it reads latest[x]\textit{latest}[x] points to a DEL node, or when it performs an unsuccessful CAS. In the following two lemmas, we prove that for each of these two cases, there is a configuration during the TrieInsert(x)(x) in which xSx\in S, and hence does not need to add xx to SS. The case for TrieDelete(x)(x) is symmetric.

Lemma 4.2.

If uOpuOp is a TrieInsert(x)(x) operation that returns on line 18, then in the configuration CC immediately after uOpuOp reads latest[x]\textit{latest}[x], xSx\in S. If uOpuOp is a TrieDelete(x)(x) returns on line 36, then in the configuration CC immediately after uOpuOp reads latest[x]\textit{latest}[x], xSx\notin S.

Proof.

Suppose uOpuOp is a TrieInsert(x)(x) operation. Let 𝑢𝑁𝑜𝑑𝑒\mathit{uNode} be the pointed to by latest[x]\textit{latest}[x] that is read by uOpuOp. Since uOpuOp returned on line 18 (or on line 36 for TrieDelete), it saw that 𝑢𝑁𝑜𝑑𝑒.𝑡𝑦𝑝𝑒=INS\mathit{uNode}.\mathit{type}=\text{INS} (or 𝑢𝑁𝑜𝑑𝑒.𝑡𝑦𝑝𝑒=DEL\mathit{uNode}.\mathit{type}=\text{DEL} for TrieDelete). By definition, xSx\in S (or xSx\notin S) in the configuration CC immediately after this read. ∎

Lemma 4.3.

If uOpuOp is a TrieInsert(x)(x) operation that returns on line 23, then there is a configuration during uOpuOp in which xSx\in S. If uOpuOp is a TrieDelete(x)(x) operation that returns on line 41, then there is a configuration during uOpuOp in which xSx\notin S.

Proof.

We prove the case when uOpuOp is an TrieInsert(x)(x) operation. The case when uOpuOp is a TrieDelete(x)(x) operation follows similarly.

Since uOpuOp does not return on line 18, it read that latest[x]\textit{latest}[x] points to a DEL node. From the code, latest[x]\textit{latest}[x] can only change from pointing to this DEL node to an INS node by a successful CAS of some TrieInsert(x)(x) operation. Since uOpuOp performs an unsuccessful CAS on line 155, some other TrieInsert(x)(x) changed latest[x]\textit{latest}[x] to point to an INS node using a successful CAS sometime between uOpuOp’s read of latest[x]\textit{latest}[x] and uOpuOp’s unsuccessful CAS on latest[x]\textit{latest}[x]. In the configuration immediately after this successful CAS, xSx\in S. ∎

Lemma 4.4.

The implementation of the relaxed binary trie is strongly linearizable with respect to TrieInsert, TrieDelete, and TrieSearch operations.

Proof.

Consider an execution α\alpha of the relaxed binary trie. Let α\alpha^{\prime} be any prefix of α\alpha. Let opop be a TrieSearch, TrieInsert, or TrieDelete operation in α\alpha.

Suppose opop is a TrieSearch(x)(x) operation. This operation is linearized in the configuration CC immediately after opop reads latest[x]\textit{latest}[x]. By Lemma 4.1, opop returns True if and only if xSx\in S in CC. If α\alpha^{\prime} contains CC, then opop is linearized at CC for both α\alpha and α\alpha^{\prime}.

Suppose opop is a TrieInsert(x)(x) operation. Suppose opop reads that latest[x]\textit{latest}[x] points to a INS node (on line 17) during α\alpha^{\prime} and, hence, returns without changing latest[x]\textit{latest}[x]. By Lemma 4.2, xSx\in S in the configuration CC immediately after this read. Therefore, if α\alpha^{\prime} contains CC, then opop is linearized at CC for both α\alpha and α\alpha^{\prime}.

So opop reads that latest[x]\textit{latest}[x] points to a DEL node. If opop successfully changes latest[x]\textit{latest}[x] to point to its own update node using CAS, then opop is linearized at this CAS step. If α\alpha^{\prime} contains this CAS step, then opop is linearized at this CAS step for both α\alpha and α\alpha^{\prime}.

If opop does not successfully change latest[x]\textit{latest}[x] to point to its own update node using CAS, then by Lemma 4.3, there is a configuration sometime between opop’s read of latest[x]\textit{latest}[x] and its unsuccessful CAS in which xSx\in S. Consider the earliest such configuration CC, so CC immediately follows the successful CAS of some TrieInsert(x)(x) operation. If α\alpha^{\prime} contains this successful CAS (and hence CC), then opop performs an unsuccessful CAS in any continuation of α\alpha^{\prime}. So opop is linearized at CC for both α\alpha and α\alpha^{\prime}.

The case when opop is a TrieDelete(x)(x) operation follows similarily. ∎

4.5.2 Properties of the Interpreted Bits

In this section, we prove that properties IB0 and IB0 of the interpreted bits are satisfied by our implementation. We say that the interpreted bit of a binary trie node tt is accurate if it is the OR of the interpreted bits of the leaves in the subtrie rooted at tt. In all configurations CC and all binary trie nodes tt, either the interpreted bit of tt is accurate or there is an active update operation in CC that may change the interpreted bit of tt to be accurate.

We begin with basic observations and lemmas about how the fields of DEL nodes change.

Observation 4.5.

Only TrieDelete operations change 𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟\mathit{dNodePtr} of a binary trie node and only change it to point to their own DEL node (on line 53 or 57).

Observation 4.6.

For any binary trie node tt, suppose t.𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟t.\mathit{dNodePtr} is changed to point from a DEL node 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} to a different DEL node. Then in any future configuration, t.𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟t.\mathit{dNodePtr} does not point to 𝑑𝑁𝑜𝑑𝑒\mathit{dNode}.

Observation 4.7.

Let 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} be the DEL node created by a TrieDelete(x)(x) operation opop. Only opop writes to 𝑑𝑁𝑜𝑑𝑒.upper0Boundary\mathit{dNode}.\mathit{upper0Boundary} and it only does so on line 59.

Lemma 4.8.

Let 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} be a DEL node created by a TrieDelete(x)(x) operation opop. Suppose opop changes t.𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟t.\mathit{dNodePtr} of some binary trie node tt to point to 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} due to a successful CAS ss (performed on line 53 or 57). Before ss, opop has performed a sequence of successful CASs updating each binary trie node from the parent of the leaf with key xx to a child of tt to point to 𝑑𝑁𝑜𝑑𝑒\mathit{dNode}. Immediately after ss, 𝑑𝑁𝑜𝑑𝑒.upper0Boundary=t.0pt1\mathit{dNode}.\mathit{upper0Boundary}=t.0pt-1.

Proof.

Let CC be the configuration immediately after the CAS that changes t.𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟t.\mathit{dNodePtr} to point to 𝑑𝑁𝑜𝑑𝑒\mathit{dNode}. Let h=t.0pth=t.0pt. Let t0,,tht_{0},\dots,t_{h} be the sequence of nodes on the path from the leaf t0t_{0} with key xx to th=tt_{h}=t.

Suppose that tht_{h} is the parent of the leaf with key xx, so t.0pt1=0t.0pt-1=0. The initial value of 𝑑𝑁𝑜𝑑𝑒.upper0Boundary\mathit{dNode}.\mathit{upper0Boundary} is 0. In the first iteration of BinaryTrieDelete, opop does not change 𝑑𝑁𝑜𝑑𝑒.upper0Boundary\mathit{dNode}.\mathit{upper0Boundary} prior to performing cascas. Therefore, in CC, 𝑑𝑁𝑜𝑑𝑒.upper0Boundary=0\mathit{dNode}.\mathit{upper0Boundary}=0.

So suppose that tht_{h} is not the parent of the leaf with key xx. From the code of BinaryTrieDelete, opop does not proceed to its next iteration unless it performs at least one successful CAS on either line 53 or 57. Hence, prior to CC, opop has performed a sequence of successful CASs, updating the ti.𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟t_{i}.\mathit{dNodePtr} to point to 𝑑𝑁𝑜𝑑𝑒\mathit{dNode}, for 1ih11\leq i\leq h-1.

In the iteration prior to the iteration opop performs a successful CAS on tht_{h}, opop updates 𝑑𝑁𝑜𝑑𝑒.upper0Boundary\mathit{dNode}.\mathit{upper0Boundary} to th1.0pt=t.0pt1t_{h-1}.0pt=t.0pt-1 on line 59. By Observation 4.7, no operation besides opop change 𝑑𝑁𝑜𝑑𝑒.upper0Boundary\mathit{dNode}.\mathit{upper0Boundary} between this write and CC. Hence, in CC, 𝑑𝑁𝑜𝑑𝑒.upper0Boundary=t.0pt1\mathit{dNode}.\mathit{upper0Boundary}=t.0pt-1.

Lemma 4.9.

Suppose 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} and 𝑑𝑁𝑜𝑑𝑒\mathit{dNode}^{\prime} were the DEL nodes created by TrieDelete(x)(x) operations, opop and opop^{\prime}, respectively. Suppose that 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} is activated before 𝑑𝑁𝑜𝑑𝑒\mathit{dNode}^{\prime}. Once opop^{\prime} performs a successful CAS on the 𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟\mathit{dNodePtr} of some binary trie node, then opop does not perform a successful CAS on the 𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟\mathit{dNodePtr} of that same binary trie node.

Proof.

Let CC^{\prime} be the configuration immediately after opop^{\prime}’s successful CAS on t.𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟t.\mathit{dNodePtr} of some binary trie node tt. This only occurs after 𝑑𝑁𝑜𝑑𝑒\mathit{dNode}^{\prime} is activated by opop^{\prime}. Then in CC^{\prime}, 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} is not the first activated node in latest[x]\textit{latest}[x].

Suppose opop is performs a CAS on t.𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟t.\mathit{dNodePtr} in some configuration after CC^{\prime}. Before this CAS, opop read that 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} is the first activated node latest[x]\textit{latest}[x] on line 53 or 57.

This read must occur before CC^{\prime}, since 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} is not the first activated node in latest[x]\textit{latest}[x] in CC^{\prime}. Prior to this read, opop^{\prime} reads 𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟\mathit{dNodePtr} (on line 50 or 54). Hence, opop’s last read of 𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟\mathit{dNodePtr} also occurred before CC^{\prime}. Then opop^{\prime} changes the value of t.𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟t.\mathit{dNodePtr} to a value different than what was last read by opop. So the CAS that opop is performs on t.𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟t.\mathit{dNodePtr} will be unsuccessful. ∎

Lemma 4.10.

Let 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} be the DEL node created by a TrieDelete(x)(x) operation opop. Suppose opop changes t.𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟t.\mathit{dNodePtr} of some binary trie node tt to point to 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} (due to a successful CAS on line 53 or 57). In the configuration immediately after this CAS, the interpreted bit of tt is 1.

Proof.

Let cascas be the successful CAS operation where opop updates tt to point to 𝑑𝑁𝑜𝑑𝑒\mathit{dNode}. Let CC be the configuration immediately after this CAS.

Suppose, for contradiction, that the interpreted bit of tt is 0 in CC. The interpreted bit of tt depends on the first activated update node in latest[x]\textit{latest}[x]. The first activated update node must be a DEL node, otherwise the interpreted bit of tt is 1.

Let 𝑑𝑁𝑜𝑑𝑒\mathit{dNode}^{\prime} be the first activated update node in latest[x]\textit{latest}[x] in CC. The interpreted bit of tt is 0 when t.0pt<𝑑𝑁𝑜𝑑𝑒.lower1Boundaryt.0pt<\mathit{dNode}^{\prime}.\mathit{lower1Boundary} and t.0pt𝑑𝑁𝑜𝑑𝑒.upper0Boundaryt.0pt\leq\mathit{dNode}^{\prime}.\mathit{upper0Boundary}. By Lemma 4.8, 𝑑𝑁𝑜𝑑𝑒.upper0Boundary=t.0pt1\mathit{dNode}.\mathit{upper0Boundary}=t.0pt-1, so 𝑑𝑁𝑜𝑑𝑒𝑑𝑁𝑜𝑑𝑒\mathit{dNode}^{\prime}\neq\mathit{dNode}. So 𝑑𝑁𝑜𝑑𝑒\mathit{dNode}^{\prime} is a DEL node created by a TrieDelete(x)(x) operation opopop^{\prime}\neq op. Since 𝑑𝑁𝑜𝑑𝑒\mathit{dNode}^{\prime} is the first activated update node in latest[x]\textit{latest}[x] in CC, 𝑑𝑁𝑜𝑑𝑒\mathit{dNode}^{\prime} is activated and inserted into latest[x]\textit{latest}[x] after 𝑑𝑁𝑜𝑑𝑒\mathit{dNode}.

Since 𝑑𝑁𝑜𝑑𝑒.upper0Boundaryt.0pt\mathit{dNode}^{\prime}.\mathit{upper0Boundary}\geq t.0pt, this implies opop^{\prime} performed a successful CAS on t.𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟t.\mathit{dNodePtr}, updating it to point to 𝑑𝑁𝑜𝑑𝑒\mathit{dNode}^{\prime} sometime before CC. Lemma 4.9 implies cascas will be unsuccessful, a contradiction.

For a binary trie node tt, recall that UtU_{t} is the set of keys of the leaves of the subtrie rooted at tt. Let the latest update operation with key xx in configuration CC be the update operation that created the first activated update node in latest[x]\textit{latest}[x] in CC. If the first activated update node in latest[x]\textit{latest}[x] is a dummy node, we assume its latest update operation is a completed, dummy TrieDelete(x)(x) operation.

Let opop be the latest TrieInsert operation with key xUtx\in U_{t} in configuration CC. We say that opop has completed iteration tt of InsertBinaryTrie if opop performed a MinWrite with value t.heightt.height on line 33, read that 𝑢𝑁𝑜𝑑𝑒.𝑡𝑦𝑝𝑒\mathit{uNode}.\mathit{type} has type INS on line 29 during iteration tt of InsertBinaryTrie, or read that t.0pt𝑢𝑁𝑜𝑑𝑒.lower1Boundaryt.0pt\geq\mathit{uNode}.\mathit{lower1Boundary} or t.0pt>𝑢𝑁𝑜𝑑𝑒.upper0Boundaryt.0pt>\mathit{uNode}.\mathit{upper0Boundary} on line 30 during iteration tt of InsertBinaryTrie. Note that if opop returns while performing iteration tt of InsertBinaryTrie, it does not complete iteration tt. We say that opop has a potential update to tt if opop has not yet invoked InsertBinaryTrie, or it has invoked InsertBinaryTrie but has not returned and has not completed iteration tt.

Lemma 4.11.

Suppose opop is a latest TrieDelete operation that created a DEL node vv, and suppose opop is in iteration tt of DeleteBinaryTrie in a configuration CC. Then it is not possible for opop to complete iteration tt of DeleteBinaryTrie from CC if and only if either v.stop=Truev.\textit{stop}=\textsc{True} and there is a possible check to v.stopv.\textit{stop}, v.lower1Boundary=b+1v.\mathit{lower1Boundary}=b+1 and there is a possible check to v.lower1Boundaryv.\mathit{lower1Boundary}, or t.𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟t.\mathit{dNodePtr} has changed since opop’s last read of it on line 54.

Proof.

Suppose v.stop=Truev.\textit{stop}=\textsc{True} and there is a possible check to v.stopv.\textit{stop}, v.lower1Boundary=b+1v.\mathit{lower1Boundary}=b+1 and there is a possible check to v.lower1Boundaryv.\mathit{lower1Boundary}, or t.𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟t.\mathit{dNodePtr} has changed since opop’s last read of it on line 54. Then in any continuation from CC, opop will return when it does its next read to v.stopv.\textit{stop}, v.lower1Boundaryv.\mathit{lower1Boundary}, or performs its next CAS, or return earlier.

Suppose v.stop=Falsev.\textit{stop}=\textsc{False} or there is no possible check to v.stopv.\textit{stop}, v.lower1Boundaryb+1v.\mathit{lower1Boundary}\neq b+1 or there no is a possible check to v.lower1Boundaryv.\mathit{lower1Boundary}, and t.𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟t.\mathit{dNodePtr} has not changed since opop’s last read of it on line 54. In opop’s solo continuation from CC, opop will complete iteration tt. ∎

Observation 4.12.

Suppose an update operation opop with key xx completes iteration tt (of InsertBinaryTrie for TrieInsert operations or DeleteBinaryTrie for TrieDelete operations). Then for each node tt^{\prime} on the path from the parent of the leaf with key xx to tt, opop has completed iteration tt^{\prime}.

Proof.

From the code of InsertBinaryTrie and DeleteBinaryTrie, if opop does not complete an iteration, it returns from InsertBinaryTrie and DeleteBinaryTrie. Since opop completes iterations up the binary trie, starting from the parent of the leaf with key xx to the root, opop has previously completed iterations for all internal binary trie nodes on the path to tt. ∎

Lemma 4.13.

Let 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} be the DEL node created by a TrieDelete operation opop with key xx. Suppose 𝑑𝑁𝑜𝑑𝑒.upper0Boundary>0\mathit{dNode}.\mathit{upper0Boundary}>0. Then for each binary trie node tt such that xUtx\in U_{t} and t.0pt𝑑𝑁𝑜𝑑𝑒.upper0Boundaryt.0pt\leq\mathit{dNode}.\mathit{upper0Boundary}, opop has completed iteration tt of DeleteBinaryTrie.

Proof.

By Observation 4.7, only the Delete operation that created 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} writes to opop. From the code of DeleteBinaryTrie, each completed iteration of DeleteBinaryTrie increases 𝑑𝑁𝑜𝑑𝑒.upper0Boundary\mathit{dNode}.\mathit{upper0Boundary} by 1. By definition, opop completes iteration tt of DeleteBinaryTrie immediately after it writes t.0ptt.0pt to 𝑑𝑁𝑜𝑑𝑒\mathit{dNode}. Hence, opop has completed iteration tt of DeleteBinaryTrie for each node tt where xUtx\in U_{t} and t.0pt𝑑𝑁𝑜𝑑𝑒.upper0Boundaryt.0pt\leq\mathit{dNode}.\mathit{upper0Boundary}. ∎

Lemma 4.14.

Suppose an TrieInsert operation is poised to perform a MinWrite of t.0ptt.0pt to 𝑑𝑁𝑜𝑑𝑒.lower1Boundary\mathit{dNode}.\mathit{lower1Boundary} for some DEL node, 𝑑𝑁𝑜𝑑𝑒\mathit{dNode}, during iteration tt of InsertBinaryTrie. Then the TrieDelete operation that created 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} has previously completed iteration tt of DeleteBinaryTrie.

Proof.

Let the TrieInsert operation poised to perform the MinWrite be opop. Since opop is poised to perform the MinWrite, it read that t.0pt𝑑𝑁𝑜𝑑𝑒.upper0Boundaryt.0pt\leq\mathit{dNode}.\mathit{upper0Boundary} on line 30 of InsertBinaryTrie. By Observation 4.13, the TrieDelete operation that created 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} has completed iteration tt of DeleteBinaryTrie. ∎

Lemma 4.15.

Suppose an internal binary trie node tt has interpreted bit 0 in a configuration CC, and suppose t.𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟t.\mathit{dNodePtr} points to an DEL node with key xx. Then the latest update operation with key xx has completed iteration tt of DeleteBinaryTrie.

Proof.

Let opop be the latest update operation with key xx. Since the interpreted bit of tt is 0, the first activated update node in latest[x]\textit{latest}[x] is a DEL node, 𝑑𝑁𝑜𝑑𝑒\mathit{dNode}, which was created by opop. Furthermore, t.0pt𝑑𝑁𝑜𝑑𝑒.upper0Boundaryt.0pt\leq\mathit{dNode}.\mathit{upper0Boundary}. By Observation 4.7, only opop writes to 𝑑𝑁𝑜𝑑𝑒.upper0Boundary\mathit{dNode}.\mathit{upper0Boundary}. Since 𝑑𝑁𝑜𝑑𝑒.upper0Boundary\mathit{dNode}.\mathit{upper0Boundary} and opop completes iteration tt of DeleteBinaryTrie immediately after it writes t.0ptt.0pt to 𝑑𝑁𝑜𝑑𝑒.upper0Boundary\mathit{dNode}.\mathit{upper0Boundary}, it follows that opop has completed iteration tt of DeleteBinaryTrie. ∎

Observation 4.16.

Let 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} be the INS node created by an TrieInsert operation opop. Then only opop writes to 𝑖𝑁𝑜𝑑𝑒.target\mathit{iNode}.\textit{target}.

The following lemma implies that, in all configurations CC and all binary trie nodes tt, either the interpreted bit of tt is accurate or there exists a key xx in UtU_{t} whose latest update operation has a potential update to tt. It implies that property IB1 of the interpreted bits is satisfied.

Lemma 4.17.

If there exists a key in UtU_{t} whose latest update operation, iOpiOp, is an TrieInsert operation, and iOpiOp has completed iteration tt of InsertBinaryTrie, then tt has interpreted bit 1.

Proof.

We prove by induction on the configurations of the execution. In the initial configuration, the latest update operation of every key in UU is a dummy TrieDelete operation. So the lemma is vacuously true.

Suppose the lemma is true in a configuration CC^{\prime} immediately before a step ss by an operation opop, and we show that it is true in the following configuration CC.

  • Suppose ss is a step that activates an INS node in latest[x]\textit{latest}[x]. So opop is now the latest update operation with key xx.

    Let tt be any node in the binary trie where xUtx\in U_{t}. The interpreted bit of tt does not change as a result of ss unless t.𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟.key=xt.\mathit{dNodePtr}.key=x. In this case, the first activated update node in latest[x]\textit{latest}[x] is an INS node, so by definition tt has interpreted bit 1 in CC.

  • Suppose ss is a step that activates an DEL node, 𝑑𝑁𝑜𝑑𝑒\mathit{dNode}, in latest[x]\textit{latest}[x]. So opop is now the latest update operation with key xx.

    Let tt be any internal node in the binary trie where xUtx\in U_{t}. The interpreted bit of tt does not change as a result of ss unless t.𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟.key=xt.\mathit{dNodePtr}.key=x. Since 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} is a newly activated DEL node, 𝑑𝑁𝑜𝑑𝑒.upper0Boundary=0\mathit{dNode}.\mathit{upper0Boundary}=0. Since t.0pt1t.0pt\geq 1, it follows by definition that tt has interpreted bit 1 in CC.

  • Suppose ss is a successful CAS that changes t.𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟t.\mathit{dNodePtr} from an DEL node with key xx^{\prime} to a DEL node with key xx.

    Only the interpreted bit of tt may change as a result of ss. By Lemma 4.10, the interpreted bit of tt is 1 in CC.

  • Suppose ss is a step in which opop completes iteration tt of InsertBinaryTrie (as a result of a read on line 29 or line 30, or a MinWrite of t.0ptt.0pt to 𝑑𝑁𝑜𝑑𝑒.lower1Boundary\mathit{dNode}.\mathit{lower1Boundary}).

    Let 𝑢𝑁𝑜𝑑𝑒\mathit{uNode} be the update node returned by FindLatest on line 28 during iteration tt of opop InsertBinaryTrie prior to ss. Let x=𝑢𝑁𝑜𝑑𝑒.keyx=\mathit{uNode}.key. By Lemma 5.3, there is a configuration C′′C^{\prime\prime} during FindLatest in which 𝑢𝑁𝑜𝑑𝑒\mathit{uNode} is the first activated update node in latest[x]\textit{latest}[x]. If t.𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟.key=𝑢𝑁𝑜𝑑𝑒.keyt.\mathit{dNodePtr}.key=\mathit{uNode}.key and 𝑢𝑁𝑜𝑑𝑒\mathit{uNode} is still the first activated update node in CC, then the interpreted bit of tt is 1 after ss and opop completes iteration tt of InsertBinaryTrie.

    So suppose in CC, the first activated update node in latest[t.𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟.key]\textit{latest}[t.\mathit{dNodePtr}.key] is an update node other than 𝑢𝑁𝑜𝑑𝑒\mathit{uNode}. Then a successful CAS has changed t.𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟.keyt.\mathit{dNodePtr}.key sometime between C′′C^{\prime\prime} and CC. These steps change the interpreted bit of tt to 1.

  • Suppose ss is a write of t.0ptt.0pt to 𝑑𝑁𝑜𝑑𝑒.upper0Boundary\mathit{dNode}.\mathit{upper0Boundary} on line 59 of DeleteBinaryTrie.

    Suppose the lemma holds in CC^{\prime} because some TrieInsert operation opop^{\prime} has completed iteration tt in a previous configuration C′′C^{\prime\prime}. Let C′′′C^{\prime\prime\prime} be the configuration before C′′C^{\prime\prime} in which opop^{\prime} completed iteration tt^{\prime}, where tt^{\prime} is a child of tt. By the induction hypothesis, the interpreted bit of tt is 1 in all configurations from C′′C^{\prime\prime} to CC^{\prime}, and the interpreted bit of tt^{\prime} is 1 in all configurations from C′′′C^{\prime\prime\prime} to CC^{\prime}.

    Since opop is poised to perform a write of t.0ptt.0pt to 𝑑𝑁𝑜𝑑𝑒.upper0Boundary\mathit{dNode}.\mathit{upper0Boundary} in CC^{\prime}, this implies that opop has previously successfully read that tt^{\prime} has interpreted bit 0 on line 58 of DeleteBinaryTrie prior to C′′′C^{\prime\prime\prime}. Furthermore, opop has previously performed a successful CAS updating t.𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟t.\mathit{dNodePtr} to point to 𝑑𝑁𝑜𝑑𝑒\mathit{dNode}. The step ss only affects opop if t.𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟t.\mathit{dNodePtr} still points to 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} and 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} is the first activated update node in its latest list. So when opop^{\prime} completes iteration tt, it either saw t.0pt<𝑑𝑁𝑜𝑑𝑒.lower1Boundaryt.0pt<\mathit{dNode}.\mathit{lower1Boundary} or it performed a MinWrite of t.0ptt.0pt to 𝑑𝑁𝑜𝑑𝑒.lower1Boundary\mathit{dNode}.\mathit{lower1Boundary}. So t.0pt𝑑𝑁𝑜𝑑𝑒.lower1Boundaryt.0pt\geq\mathit{dNode}.\mathit{lower1Boundary} in CC, and hence the interpreted bit of tt is 1 in CC.

We now focus on showing that property IB0 of the interpreted bits is satisfied. Let opop be the latest TrieDelete operation with key xUtx\in U_{t} in configuration CC. Let vv be the DEL node created by opop. We say that opop has completed iteration tt of DeleteBinaryTrie(v)(v) if opop performed the write with value t.0ptt.0pt to v.upper0Boundaryv.\mathit{upper0Boundary} on line 59 of DeleteBinaryTrie.

We say that opop is flagged if v.stop=Truev.\textit{stop}=\textsc{True} or v.lower1Boundaryb+1v.\mathit{lower1Boundary}\neq b+1. We say that opop has a potential update to tt in configuration CC if opop is not flagged in CC and there exists an execution from CC in which opop writes the value t.0ptt.0pt to v.upper0Boundaryv.\mathit{upper0Boundary} on line 59 of DeleteBinaryTrie while t.𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟t.\mathit{dNodePtr} points to vv. By definition, a dummy TrieDelete operation does not have a potential update to any binary trie node in any configuration.

Consider an configuration CC and binary trie node tt in which all the latest update operations with keys in UtU_{t} are Delete operations. Let D(t,C)D(t,C) denote the earliest configuration before CC in which these operations have been invoked. Let OP(t,C)OP(t,C) be the set of latest TrieDelete operations with keys in UtU_{t} that have a potential update to tt in D(t,C)D(t,C). Note that in the step immediately before D(t,C)D(t,C), a TrieDelete operation with key in UtU_{t} activates its DEL node, becoming a latest TrieDelete operation. This operation has a potential update to tt, so OP(t,C)OP(t,C) is non-empty.

Lemma 4.18.

Consider a configuration CC and a binary trie node tt such that the latest update operations for all keys in UtU_{t} are TrieDelete operations. Operations in OP(t,C)OP(t,C) do not become flagged in any step between D(t,C)D(t,C) and CC by an operation with a key in UtU_{t}.

Proof.

Suppose, for contradiction, that an operation opOP(t,C)op\in OP(t,C) is flagged by a step ss by an operation opop^{\prime} with key xUtx^{\prime}\in U_{t}, where ss is between D(t,C)D(t,C) and CC. Let vv and vv^{\prime} be the DEL node created by opop and opop^{\prime}, respectively. Let xx be the key of opop.

Suppose opop is flagged because opop^{\prime} does a MinWrite to v.lower1Boundaryv.\mathit{lower1Boundary}. Prior to this MinWrite, opop^{\prime} reads that its update node is the first activated in latest[x]\textit{latest}[x^{\prime}] on line 32, which is before D(t,C)D(t,C). It also set v.target=vv^{\prime}.\textit{target}=v on line 31. Therefore, before the latest TrieDelete(x)(x^{\prime}) operation invokes DeleteBinaryTrie, it reads v.target=vv^{\prime}.\textit{target}=v and sets v.stop=Truev.\textit{stop}=\textsc{True}. Since opop is flagged before D(t,C)D(t,C), opop does not have a potential update to tt in D(t,C)D(t,C). So opOP(t,C)op\notin OP(t,C), a contradiction.

Suppose opop is flagged because opop^{\prime} writes v.stop=Truev.\textit{stop}=\textsc{True}. Then opop read v.target=vv^{\prime}.\textit{target}=v, where vv^{\prime} is the INS node of some TrieInsert(x)(x^{\prime}) operation where xUtx^{\prime}\in U_{t}. Then opop^{\prime} is a TrieDelete operation invoked after opop. So opOP(t,C)op^{\prime}\in OP(t,C). But opop^{\prime} writes v.stop=Truev.\textit{stop}=\textsc{True} before it invokes DeleteBinaryTrie, which is before D(t,C)D(t,C). Since opop is flagged before D(t,C)D(t,C), opop does not have a potential update to tt in D(t,C)D(t,C). So opOP(t,C)op\notin OP(t,C), a contradiction. ∎

The next lemma implies that property IB0 is satisfied by the interpreted bits.

Lemma 4.19.

Consider any configuration CC and binary trie node tt. If the latest update operation for all keys in UtU_{t} in CC are TrieDelete operations and none of these operations have a potential update to tt in CC, then tt has interpreted bit 0 in CC and t.𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟t.\mathit{dNodePtr} points to a DEL node created by an operation in OP(t,C)OP(t,C).

Proof.

The proof is by induction on the configurations of the execution and the height of binary trie nodes.

In the initial configuration, the latest update operation of every key in UU is a dummy TrieDelete operation and each latest list points to a dummy update node. The fields of the dummy update nodes are initialized so that the interpreted bits of all binary trie nodes are 0. Since the dummy operations do not have a potential update to any binary trie node, the claim is true in the initial configuration.

Consider any other reachable configuration CC. Let CC^{\prime} be the configuration immediately before CC in some execution and let ss be the step performed in CC^{\prime} that results in CC. We assume that the claim holds for all binary trie nodes in CC^{\prime}. Suppose ss is performed by operation opop with key xUtx\in U_{t}.

Consider a leaf tt of the binary trie with key xx. No operations have a potential update to tt in any configuration. If the latest update operation with key xx is a TrieDelete operation in CC, then the first activated update node in latest[x]\textit{latest}[x] is a TrieDelete operation. By definition, the interpreted bit of tt is 0.

Now suppose when tt is the parent of a leaf. Let opop_{\ell} and oprop_{r} be the latest TrieDelete operations with keys xx and x+1x+1 in CC. Since opop and opop^{\prime} have not yet completed any iterations of DeleteBinaryTrie, opop_{\ell} and oprop_{r} are not flagged. Non-latest update operations with keys in {x,x+1}\{x,x+1\} in CC can only cause operations in OP(t,C)OP(t,C) to perform one unsuccessful CAS. Since operations in OP(t,C)OP(t,C) perform at least two CASs during iteration tt of DeleteBinaryTrie, at least one operation in OP(t,C)OP(t,C) will perform a successful CAS.

Now suppose tt is an internal binary trie node that is not the parent of a leaf. We assume the claim is true in CC for all binary trie nodes that are proper descendants of tt.

By definition, step ss can change the interpreted bit of tt if and only if it is a successful CAS that changes t.𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟t.\mathit{dNodePtr}, a write to v.lower1Boundaryv.\mathit{lower1Boundary} or v.upper0Boundaryv.\mathit{upper0Boundary}, where vv is the first activated DEL node in latest[t.𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟.𝑘𝑒𝑦]\textit{latest}[t.\mathit{dNodePtr}.\mathit{key}], or a successful CAS that activates a new update node in latest[x]\textit{latest}[x], where x=t.𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟.𝑘𝑒𝑦x=t.\mathit{dNodePtr}.\mathit{key}.

Step ss can flag an operation if and only if it is a write to v.stopv.\textit{stop} or v.lower1Boundaryv.\mathit{lower1Boundary} for some DEL node vv. This step may also result in the operation that created vv from no longer having a potential update to tt, as it is guaranteed to return the next time they read v.stopv.\textit{stop} or v.lower1Boundaryv.\mathit{lower1Boundary}.

Step ss can cause opop to return from DeleteBinaryTrie if it reads v.stop=Truev.\textit{stop}=\textsc{True}, v.lower1Boundaryb+1v.\mathit{lower1Boundary}\neq b+1, results in FirstActivated(v)(v) returns False, or an unsuccessful CAS on t.𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟t.\mathit{dNodePtr}.

When ss is a step where opop returns from DeleteBinaryTrie because InterpretedBit(t)(t^{\prime}) returns 1, opop may no longer have a potential update to tt in CC.

  • Suppose ss is a step that activates an INS node in latest[x]\textit{latest}[x], where x=t.𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟.keyx=t.\mathit{dNodePtr}.key. In this case, opop is an TrieInsert operation. So opop is now the latest update operation with key xx. Since the latest update operations for all keys in UtU_{t} are no longer TrieDelete, the claim is vacuously true for tt in CC.

  • Suppose ss is a step that activates a DEL node vv in latest[x]\textit{latest}[x], where x=t.𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟.keyx=t.\mathit{dNodePtr}.key. Since vv is newly activated, v.lower1Boundary=b+1v.\mathit{lower1Boundary}=b+1 and v.stop=Falsev.\textit{stop}=\textsc{False}. Then the latest update operation with key xx is non-flagged and has a potential update to tt. So the claim is vacuously true for tt in CC.

  • Suppose ss is a step where opop returns because it reads that its update node vv is not the first activated update node on line 51 or on line 55 during iteration tt of DeleteBinaryTrie.

    Then prior to ss, an TrieInsert(x)(x) operation has inserted an activated INS node into latest[x]\textit{latest}[x] sometime after vv was inserted into latest[x]\textit{latest}[x]. So opop is not the latest update operation with key xx in CC^{\prime}.

    By induction hypothesis, since the claim is true for tt in CC^{\prime}, it is also true for tt in CC.

  • Suppose ss is a step where opop returns because it reads v.stop=Truev.\textit{stop}=\textsc{True} or v.upper0Boundaryb+1v.\mathit{upper0Boundary}\neq b+1 on line 52 or line 56 during iteration tt of DeleteBinaryTrie.

    By definition, opop is flagged in CC^{\prime} and CC and does not have a potential update to tt in CC^{\prime} and CC. Hence, the claim is vacuously true for tt in CC^{\prime} and CC.

  • Suppose ss is the second unsuccessful CAS performed by opop during iteration tt of DeleteBinaryTrie.

    Then t.𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟t.\mathit{dNodePtr} has changed since opop’s last read of t.𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟t.\mathit{dNodePtr}. By definition, opop does not have a potential update to tt in CC^{\prime} and CC. Hence, the claim is vacuously true for tt in CC^{\prime} and CC.

  • Suppose ss is a successful CAS that changes t.𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟t.\mathit{dNodePtr}.

    If there are still latest update operations with keys in UtU_{t} that have a potential update to tt in CC, the claim is vacuously true for tt in CC. So in CC, suppose no latest update operations have a potential update to tt. Note that if there is a latest update operation with a key in UtU_{t} with a potential update to tt in CC^{\prime} that is in iteration tt^{\prime} of DeleteBinaryTrie, where tt^{\prime} is a descendant of tt, then this operation will still have a potential update to tt in CC. So all latest update operations with keys in UtU_{t} are in iteration tt of DeleteBinaryTrie. Furthermore, these operations either already performed a successful CAS during iteration tt of DeleteBinaryTrie or have read t.𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟t.\mathit{dNodePtr} on line 54 but not yet attempted their second CAS during iteration tt of DeleteBinaryTrie.

    Suppose that in CC^{\prime}, opop is the latest update operation with key xx and opop is not flagged. By the induction hypothesis, the children of tt in CC^{\prime} have interpreted bit 0. So in opop’s solo continuation in CC, opop completes iteration tt. Then opop has a potential update to tt in CC. So the claim is vacuously true for tt in CC.

    Suppose that in CC^{\prime}, opop is not the latest update operation with key xx in CC^{\prime}. Prior to opop performing ss, opop reads t.𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟t.\mathit{dNodePtr} and reads that FirstActivated(v)=True\textsc{FirstActivated}(v)=\textsc{True} on line 51 or line 55. So there a latest TrieDelete(x)\textsc{TrieDelete}(x) operation opop^{\prime} that activates a DEL node into latest[x]\textit{latest}[x] sometime after opop performs reads that FirstActivated(v)=True\textsc{FirstActivated}(v)=\textsc{True}. So D(t,C)D(t,C) occurs after opop reads t.𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟t.\mathit{dNodePtr}.

    Let tt_{\ell} and trt_{r} be the left and right children of tt, respectively. Note that both D(t,C)D(t_{\ell},C) and D(tr,C)D(t_{r},C) occur after at or before D(t,C)D(t,C). By the induction hypothesis, tt_{\ell} and trt_{r} have interpreted bit 0 in CC^{\prime}. Without loss of generality, suppose tt_{\ell} is the last child of tt to have its 𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟\mathit{dNodePtr} changed before CC^{\prime}, and suppose t.𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟t_{\ell}.\mathit{dNodePtr} points to a DEL node vv_{\ell} created by opop_{\ell}. Since tt_{\ell} has interpreted bit 0, v.upper0Boundaryt.0ptv_{\ell}.\mathit{upper0Boundary}\geq t_{\ell}.0pt. So opop_{\ell} completes iteration tt_{\ell} sometime after D(t,C)D(t,C). By Lemma 4.18, opop_{\ell} is not flagged in CC^{\prime}. It follows that opop_{\ell} may only return from iteration tt of DeleteBinaryTrie from an unsuccessful CAS. But there are no changes to t.𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟t.\mathit{dNodePtr} from D(t,C)D(t,C) to CC^{\prime}. Furthermore, opop_{\ell} does not perform a successful CAS on tt, otherwise ss will be unsuccessful. It follows that opop_{\ell} has a potential update to tt in CC, a contradiction.

    Suppose that in CC^{\prime}, opop is the latest update operation with key xx and opop is flagged in CC^{\prime}. Prior to opop performing ss, opop reads t.𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟t.\mathit{dNodePtr} and reads that it is not flagged on line 52 or line 56. Lemma 4.18 implies opop is flagged sometime before D(t,C)D(t,C).

  • Suppose ss is a write that sets v.stopv.\textit{stop} to True on line 183 of TrieDelete to some DEL node vv with key xx. Let opop^{\prime} be the creator of vv.

    Suppose opop^{\prime} is already flagged in CC^{\prime}. Then since the claim is true for tt in CC^{\prime}, it is also true for tt in CC.

    Suppose opop^{\prime} is not already flagged in CC^{\prime}, so opop^{\prime} becomes flagged in CC as a result of ss. The step ss performed by opop occurs before opop invokes DeleteBinaryTrie. So opop has a potential update to all binary trie nodes tt where xUtx\in U_{t}. By Observation, opop is not a flagged operation. So the claim is vacuously true for tt in CC.

  • Suppose ss is a MinWrite of t.0ptt.0pt to 𝑑𝑁𝑜𝑑𝑒.lower1Boundary\mathit{dNode}.\mathit{lower1Boundary} on line 33 of InsertBinaryTrie. Let x=𝑑𝑁𝑜𝑑𝑒.keyx^{\prime}=\mathit{dNode}.key.

    Suppose opop is already flagged in CC^{\prime}. Then since the claim is true for tt in CC^{\prime}, it is also true for tt in CC.

    Suppose opop is not already flagged in CC^{\prime}, so opop becomes flagged in CC as a result of ss. Prior to ss, opop must set v.targetv.\textit{target} to 𝑑𝑁𝑜𝑑𝑒\mathit{dNode}, where vv is the INS node created by opop. If opop is the latest update operation with key xx^{\prime}, then there is a latest update operation for a key in UtU_{t} which is an TrieInsert operation, and hence the claim is vacuously true for tt in CC.

    Otherwise there is a TrieDelete(x)(x^{\prime}) operation opop^{\prime} becomes the latest update operation with key xx sometime after opop reads that vv is the first activated node on line 32, and hence after opop set v.targetv.\textit{target} to 𝑑𝑁𝑜𝑑𝑒\mathit{dNode}. Since opop is not flagged in CC^{\prime}, opop^{\prime} has not yet set 𝑑𝑁𝑜𝑑𝑒.stop=True\mathit{dNode}.\textit{stop}=\textsc{True}, and hence has not yet invoked DeleteBinaryTrie. So by definition, opop^{\prime} has a potential update to tt, where xUtx^{\prime}\in U_{t}. So the claim is vacuously true for tt in CC.

  • Suppose ss is the step where opop returns from DeleteBinaryTrie because InterpretedBit(t.𝑙𝑒𝑓𝑡)(t.\mathit{left}) or InterpretedBit(t.𝑟𝑖𝑔ℎ𝑡)(t.\mathit{right}) returns 1.

    The induction hypothesis for the children of tt implies that either is a latest TrieInsert operation with key in UtU_{t}, or there is a TrieDelete operation with a potential update to a child of tt sometime during opop’s execution of iteration tt of DeleteBinaryTrie.

  • Suppose ss completes iteration tt of DeleteBinaryTrie because it writes t.0ptt.0pt to v.upper0Boundaryv.\mathit{upper0Boundary} on line 59 of DeleteBinaryTrie.

    If opop is not the only non-flagged, latest update operation with a potential update to tt in CC^{\prime}, then the claim is vacuously true for tt in CC^{\prime} and CC.

    So opop is the only non-flagged update operation with a potential update to tt in CC^{\prime}. Since there are no non-flagged, update operations with a potential update to tt in CC, we need to show that the interpreted bit of tt is 0 in CC. Since opop is non-flagged, v.stop=Falsev.\textit{stop}=\textsc{False} and v.lower1Boundary=b+1v.\mathit{lower1Boundary}=b+1. After opop performs ss, v.upper0Boundary=t.0ptv.\mathit{upper0Boundary}=t.0pt. Furthermore, t.𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟t.\mathit{dNodePtr} points to vv. By definition, the interpreted bit of tt is 1. Since opop had a potential update to tt in all configurations from when it was invoked to CC, it follows that opOP(t,C)op\in OP(t,C).

In all cases, the claim is true for tt in CC. Therefore, it is true for all binary trie nodes in CC. ∎

4.5.3 Correctness of RelaxedPredecessor

In this section, we prove that the output of RelaxedPredecessor satisfies the specification outlined in Section 4.1. Let pOppOp be a completed instance of RelaxedPredecessor(y)(y). Let kk be the largest key that is completely present throughout pOppOp that is less than yy, or 1-1 if no such key exists.

Lemma 4.20.

In all configurations during pOppOp, for each binary trie node tt on the path from the leaf with key kk to the root, tt has interpreted bit 1.

Proof.

Since kk is completely present throughout pOppOp, for each binary trie node tt on the path from the leaf with key kk to the root, kUtSk\in U_{t}\cap S in all configurations during pOppOp. Moreover, the last SS-modifying TrieInsert(k)(k) operation linearized prior to the end of pOppOp is not concurrent with pOppOp. Hence, in all configurations during pOppOp, the last SS-modifying TrieInsert(k)(k) operation is not active. So, by Property IB1, tt has interpreted bit 1 in all configurations during pOppOp. ∎

The next two lemmas prove that the specification of RelaxedPredecessor is satisfied.

Lemma 4.21.

Suppose pOppOp returns a key xUx\in U. Then kx<yk\leq x<y and xSx\in S in some configuration during pOppOp.

Proof.

Since τ\tau returns ww, pOppOp read that the leaf \ell with key ww has interpreted bit 1. In particular, τ\tau performs an instance of InterpretedBit()(\ell) that returns 1. This means that the first activated update node in latest[w]\textit{latest}[w] returned by FindLatest(w)(w) on line 11 is an INS node. By Lemma 5.3, there is a configuration during FindLatest(w)(w) in which wSw\in S. Before pOppOp returns a key xUx\in U, it verified that the leaf with key xx had interpreted bit 1 by reading latest[x]\textit{latest}[x]. In the configuration immediately after this read, latest[x]\textit{latest}[x] points to an INS node, so xSx\in S.

Since pOppOp begins its upward traversal starting at the leaf with key yy and then performs a downward traversal starting from the left child of a binary trie node tt on this path and ending at the leaf with key xx, it follows that x<yx<y.

Consider the path of binary trie nodes from the leaf with key yy to t.𝑟𝑖𝑔ℎ𝑡t.\mathit{right}. Any node that is not on this path and is a left child of a node on this path has interpreted bit 0 when encountered by pOppOp during its upward traversal. By Lemma 4.20, each node on the path from the leaf with key kk to tt has interpreted bit 1. If kk is in the left subtrie of a proper ancestor of tt, then k<xk<x. Otherwise kk is in the left subtrie of tt. Since pOppOp traverses the right-most path of binary trie nodes with interpreted bit 1 starting from t.𝑙𝑒𝑓𝑡t.\mathit{left}, pOppOp reaches a leaf with key at least kk. Therefore, kxk\leq x. ∎

Lemma 4.22.

Suppose that, for all k<x<yk<x<y, there is no SS-modifying update operation with key xx that is linearized during pOppOp. Then pOppOp returns kk.

Proof.

Assume that, for all k<x<yk<x<y, the SS-modifying update operation with key xx that was last linearized prior to the end of pOppOp is not concurrent with pOppOp. We will prove that pOppOp returns kk\neq\bot.

By definition of kk, there are no keys greater than kk and less than yy that are completely present throughout pOppOp. By assumption, it follows that, throughout pOppOp, there are no keys in SS that are greater than kk and smaller than yy.

First suppose k=1k=-1. Recall that pOppOp begins by traversing up the relaxed binary trie starting from the leaf with key yy. Consider any node on this path whose left child tt is not on this path. Every key in UtU_{t} is less than yy, so UtS=U_{t}\cap S=\emptyset. By Property IB0, tt has interpreted bit 0 in all configurations during pOppOp. It follows that the while-loop on line 62 always evaluates to True. So pOppOp eventually reaches the root, and returns 1-1 on line 65.

Now suppose kUk\in U. Consider any binary trie node tt such that k<minUt<maxUt<yk<\min U_{t}<\max U_{t}<y. Since UtS=U_{t}\cap S=\emptyset, it follows from Property IB0 that tt has interpreted bit 0 in all configurations during pOppOp.

Let tt be the lowest common ancestor of the leaf with key kk and the leaf with key yy. Then the leaf with key kk is in the subtree rooted at t.leftt.\textit{left} and the leaf with key yy is in the subtree rooted at t.rightt.\textit{right}. Consider any node on the path from yy to t.rightt.\textit{right} whose left child tt^{\prime} is not on this path. Note that UtS=U_{t^{\prime}}\cap S=\emptyset, since k<minUt<maxUt<yk<\min U_{t^{\prime}}<\max U_{t^{\prime}}<y. By Property IB0, tt^{\prime} has interpreted bit 0 in all configurations during pOppOp. It follows from Lemma 4.20 that pOppOp reaches tt and traverses down the right-most path of binary trie nodes with interpreted bit 1 starting from t.leftt.\textit{left}.

Consider any node on the path from the leaf with key kk to t.leftt.\textit{left} whose right child tt^{\prime} is not on this path. Note that UtS=U_{t^{\prime}}\cap S=\emptyset, since k<minUt<maxUt<yk<\min U_{t^{\prime}}<\max U_{t^{\prime}}<y. By Property IB0, tt^{\prime} has interpreted bit 0 in all configurations during pOppOp. By Lemma 4.20, each binary trie node on the path from the leaf with key kk to tt has interpreted bit 1 throughout pOppOp, so pOppOp reaches the leaf with key kk and pOppOp returns kk. ∎

5 Lock-free Binary Trie

In this section, we give the full implementation of the lock-free binary trie, which uses the relaxed binary trie as one of its components. This implementation supports a linearizable Predecessor operation, unlike the RelaxedPredecessor operation of the relaxed binary trie.

In Section 5.1, we give the high-level description of our algorithms. We then describe the algorithms in detail and present the pseudocode in Section 5.2. We then prove that the implementation is linearizable in Section 5.3.

5.1 High-level Algorithm Description

One component of our lock-free binary trie is a relaxed binary trie described in Section 4. The other components are linked lists, which enable Insert, Delete, and Predecessor operations to help one another make progress.

To ensure update operations are announced at the same time as they are linearized, each update node has a status. It is initially inactive and can later change to active when the operation that created it is linearized. The array entry latest[x]\textit{latest}[x] is modified to point to a linked list of update nodes of length at most 2. When an update node is added to this list, it is inactive and it is only added to the beginning of the list. Every latest list contains at least 1 activated update node and only its first update node can be inactive. The sequence of update nodes pointed to by latest[x]\textit{latest}[x] in an execution is the history of SS-modifying TrieInsert(x)(x) and TrieDelete(x)(x) operations performed. So the types of the update nodes added to latest[x]\textit{latest}[x] alternate between INS and DEL. The first activated update node in latest[x]\textit{latest}[x] is an INS node if and only if xSx\in S.

The update announcement linked list, called the U-ALL, is a lock-free linked list of update nodes sorted by key. An update operation can add an inactive update node to the U-ALL, and is added after every update node with the same key. Then it can announce itself by activating its update node. Just before the update operation completes, it removes its update node from the U-ALL. Whenever update nodes are added and removed from the U-ALL, we additionally modify a lock-free linked list called the reverse update announcement linked list or RU-ALL. It contains a copy of all update nodes in the U-ALL, except it is sorted by keys in descending order and then by the order in which they were added. For simplicity, we assume that both the U-ALL and RU-ALL contain two sentinel nodes with keys \infty and -\infty. So U-ALL.ℎ𝑒𝑎𝑑\textit{U-ALL}.\mathit{head} always points to the sentinel node with key \infty and RU-ALL.ℎ𝑒𝑎𝑑\textit{RU-ALL}.\mathit{head} always points to the sentinel node with key -\infty.

The predecessor announcement linked list, or the P-ALL, is an unsorted lock-free linked list of predecessor nodes. Each predecessor node contains a key and an insert-only linked list, called its 𝑛𝑜𝑡𝑖𝑓𝑦𝐿𝑖𝑠𝑡\mathit{notifyList}. An update operation can notify a predecessor operation by adding a notify node to the beginning of the 𝑛𝑜𝑡𝑖𝑓𝑦𝐿𝑖𝑠𝑡\mathit{notifyList} of the predecessor operation’s predecessor node. Each Predecessor operation begins by creating a predecessor node and then announces itself by adding this predecessor node to the beginning of the P-ALL. Just before a Predecessor operation completes, it removes its predecessor node from the P-ALL.

Figure 4 shows an example of the data structure for U={0,1,2,3}U=\{0,1,2,3\}. White circles represent nodes of the relaxed binary trie. Blue rectangles represent activated INS nodes, red rectangles represent activated DEL nodes, and light red rectangles represent inactive DEL nodes. Yellow diamonds represent predecessor nodes. This example depicts 5 concurrent operations, Insert(0)(0), Insert(1)(1), Delete(3)(3), and two Predecessor operations. The data structure represents the set S={0,1,3}S=\{0,1,3\} because the first activated update node in each of latest[0]\textit{latest}[0], latest[1]\textit{latest}[1], and latest[3]\textit{latest}[3] is an INS node.

Refer to caption
Figure 4: An example of the lock-free binary trie representing S={0,1,3}S=\{0,1,3\}.

5.1.1 Insert and Delete Operations

A Search(x)(x) operation finds the first activated update node in latest[x]\textit{latest}[x], returns True if it is an INS node, and returns False if it is a DEL node.

5.1.2 Insert and Delete Operations

An Insert(x)(x) or Delete(x)(x) operation, uOpuOp, is similar to an update operation of the relaxed binary trie, with a few modifications.

Rather than changing latest[x]\textit{latest}[x] to point to its own inactive update node 𝑢𝑁𝑜𝑑𝑒\mathit{uNode}, uOpuOp instead attempts to add 𝑢𝑁𝑜𝑑𝑒\mathit{uNode} to the beginning of latest[x]\textit{latest}[x]. If successful, 𝑢𝑁𝑜𝑑𝑒\mathit{uNode} is then added to the U-ALL and RU-ALL. Next, uOpuOp changes the status of 𝑢𝑁𝑜𝑑𝑒\mathit{uNode} from inactive to active, which announces uOpuOp. Additionally, uOpuOp is linearized at this step. Any other update nodes in latest[x]\textit{latest}[x] are then removed to keep the length of latest[x]\textit{latest}[x] at most 2. If multiple update operations with key xx concurrently attempt to add an update node to the beginning of latest[x]\textit{latest}[x], exactly one will succeed. Update operations that are unsuccessful instead help the update operation that succeeded until it is linearized. Inserting into the U-ALL and RU-ALL has an amortized cost of O(c˙(op))O(\dot{c}(op)) because their lengths are at most c˙(op)\dot{c}(op).

Another modification is to notify predecessor operations after the relaxed binary trie is updated. For each predecessor node 𝑝𝑁𝑜𝑑𝑒\mathit{pNode} in the P-ALL, uOpuOp creates a notify node containing information about its update node and adds it to the beginning of 𝑝𝑁𝑜𝑑𝑒\mathit{pNode}’s 𝑛𝑜𝑡𝑖𝑓𝑦𝐿𝑖𝑠𝑡\mathit{notifyList}, provided the update node created by uOpuOp is still the first activated update node in latest[x]\textit{latest}[x]. After notifying the predecessor operations announced in the P-ALL, uOpuOp removes its update node from the U-ALL and RU-ALL before returning.

There are at most c˙(uOp)\dot{c}(uOp) predecessor nodes in the P-ALL when uOpuOp is invoked. Adding a notify node to the beginning of a 𝑛𝑜𝑡𝑖𝑓𝑦𝐿𝑖𝑠𝑡\mathit{notifyList} has an amortized cost of O(c˙(uOp))O(\dot{c}(uOp)). So the total amortized cost charged to uOpuOp for notifying these predecessor nodes is O(c˙(uOp)2)O(\dot{c}(uOp)^{2}). If a predecessor node 𝑝𝑁𝑜𝑑𝑒\mathit{pNode} is added to the P-ALL after the start of uOpuOp, the predecessor operation pOppOp that created 𝑝𝑁𝑜𝑑𝑒\mathit{pNode} pays the amortized cost of notifying 𝑝𝑁𝑜𝑑𝑒\mathit{pNode} on uOpuOp’s behalf. Since there are O(c˙(pOp))O(\dot{c}(pOp)) update operations concurrent with pOppOp when it is invoked, the total amortized cost charged to pOppOp is O(c˙(pOp)2)O(\dot{c}(pOp)^{2}).

When uOpuOp is a Delete(x)(x) operation, it also performs two embedded Predecessor(x)(x) operations, one just before uOpuOp is announced and one just before uOpuOp begins to update the relaxed binary trie. The announcement of these embedded predecessor operations remain in the P-ALL until just before uOpuOp returns. Pointers to the predecessor nodes of these embedded Predecessor(x)(x) operations and their return values are stored in 𝑢𝑁𝑜𝑑𝑒\mathit{uNode}. This information is used by other Predecessor operations.

5.1.3 Predecessor Operations

A Predecessor(y)(y) operation, pOppOp, begins by adding a predecessor node to the beginning of the P-ALL so that it can be notified by update operations. It continues to traverses the P-ALL to determine embedded predecessor operations belonging to Delete operations that have not yet completed. It then traverses the RU-ALL to identify DEL nodes corresponding to Delete operations that may have been linearized before the start of pOppOp.

Following this, pOppOp determines a number of candidate return values, which are keys in SS sometime during pOppOp, and returns the largest of these. The exact properties satisfied by the candidate return values are stated in Section 5.3.3. Some candidate return values are determined from a traversal of the relaxed binary trie, a traversal of the U-ALL, and a traversal of its notify list. The linearization point of pOppOp depends on when pOppOp encountered the value it eventually returns. The amortized cost for pOppOp to perform these traversals is O(c¯(pOp))=O(c˙(pOp))O(\bar{c}(pOp))=O(\dot{c}(pOp)).

When pOppOp encounters an update node with key x<yx<y during its traversal of the U-ALL or its notify list, it verifies that the node is the first activated update node in latest[x]\textit{latest}[x]. If a verified update node is an INS node, then xSx\in S sometime during pOppOp and xx is a candidate return value. If it is a DEL node and the Delete(x)(x) operation that created it is linearized during pOppOp, then xSx\in S immediately before this linearization point and xx is a candidate return value. The traversal of the RU-ALL is used to identify Delete operations that may have been linearized before the start of pOppOp. The keys of these operations are not added to the set of candidate return values during pOppOp’s traversal of the U-ALL and its notify list.

If the result returned by traversing the relaxed binary trie using RelaxedPredecessor(y)(y) is not \bot, then it is a candidate return value. Now suppose RelaxedPredecessor(y)(y) returns \bot. Let kk be the largest key less than yy that is completely present throughout pOppOp’s traversal of the relaxed binary trie, or 1-1 if no such key exists. By Lemma 4.22, there is an SS-modifying update operation uOpuOp with key xx, where k<x<yk<x<y, whose update to the relaxed binary trie is concurrent with pOppOp’s traversal of the relaxed binary trie. The update node created by uOpuOp is encountered by pOppOp either in the U-ALL or in its notify list. This is because either pOppOp will traverse the U-ALL before uOpuOp can remove its update node from the U-ALL, or uOpuOp will notify pOppOp before pOppOp removes its predecessor node from the P-ALL. Unless uOpuOp is a Delete(x)(x) operation that may have been linearized before the start of pOppOp, xx is a candidate return value.

Now suppose uOpuOp is a Delete(x)(x) operation linearized before the start of pOppOp. For simplicity, suppose uOpuOp is the only update operation concurrent with pOppOp. Since uOpuOp is concurrent with pOppOp’s traversal of the relaxed binary trie, its DEL node is the only update node that pOppOp encounters during its traversal of the RU-ALL. Let pOppOp^{\prime} be the first embedded Predecessor(x)(x) of uOpuOp, which was completed before uOpuOp was announced in the RU-ALL. The result returned by pOppOp^{\prime} may be added as a candidate return value for pOppOp. In addition, pOppOp traverses the notify list of pOppOp^{\prime} to possibly obtain other candidate return values. Note that kk is the predecessor of yy throughout pOppOp, so pOppOp must return kk. Let iOpiOp be the completed Insert(k)(k) operation that last added kk to SS prior to the start of pOppOp. First, suppose iOpiOp is linearized after pOppOp^{\prime} was announced. Then iOpiOp will notify pOppOp^{\prime} because iOpiOp is completed before the start of pOppOp. When pOppOp traverses the notify list of pOppOp^{\prime}, it adds kk to its set of candidate return values.

Now suppose iOpiOp is linearized before pOppOp^{\prime} was announced. Then kSk\in S throughout pOppOp^{\prime}, and pOppOp^{\prime} returns a value kk^{\prime} where kk<xk\leq k^{\prime}<x. If k=kk^{\prime}=k, then kk is added to pOppOp’s candidate return values. If kkk^{\prime}\neq k, then kk^{\prime} is removed from SS by a Delete(k)(k^{\prime}) operation prior to the start of pOppOp because kk is the predecessor of yy at the start of pOppOp. More generally, consider any Delete operation, dOpdOp, with key strictly between kk and yy that is linearized after pOppOp^{\prime} is announced and is completed before the start of pOppOp. In particular, dOpdOp’s second embedded Predecessor is completed before the start of pOppOp and returns a value which is at least kk. Before dOpdOp completed, it notified pOppOp^{\prime} of this result. We prove that there is a Delete operation that notifies pOppOp^{\prime} and whose second embedded Predecessor returns a value exactly kk. By traversing the notify list of pOppOp^{\prime}, pOppOp can determine the largest key less than yy that is in SS at the start of pOppOp. So kk is added to pOppOp’s set of candidate return values. The cost for pOppOp to traverse the notify list of pOppOp^{\prime} is O(c~(pOp))O(\tilde{c}(pOp)).

5.2 Detailed Algorithm and Pseudocode

In this section, we present a detailed description of Insert, Delete and Predecessor, as well as present their pseudocode.

Figure 5 gives a summary of the fields used by each type of node, and classifies each field as immutable, update-once, or mutable. An immutable field is set when the node is initialized and is never changed. A mutable field may change its value an arbitrary number of times. The possible transitions of other fields is specified.

78:Update Node
79:     𝑘𝑒𝑦\mathit{key} (Immutable) \triangleright A key in UU
80:     𝑡𝑦𝑝𝑒\mathit{type} (Immutable) \triangleright Either INS or DEL
81:     𝑠𝑡𝑎𝑡𝑢𝑠\mathit{status} (From Inactive to Active to Stale) \triangleright One of {Inactive,Active,Stale}\{\textsc{Inactive},\textsc{Active},\textsc{Stale}\}
82:     𝑙𝑎𝑡𝑒𝑠𝑡𝑁𝑒𝑥𝑡\mathit{latestNext} (Initialized to a pointer to an update node. Changes once to \bot)
83:     target (Mutable, initially \bot) \triangleright pointer to update node
84:     stop (From 0 to 1) \triangleright (Boolean value)
85:     completed (From 0 to 1) \triangleright (Boolean value)
86:     \triangleright Additional fields when 𝑡𝑦𝑝𝑒=DEL\mathit{type}=\text{DEL}
87:     upper0Boundary\mathit{upper0Boundary} (Mutable, initially 0) \triangleright An integer in {0,,b}\{0,\dots,b\}
88:     lower1Boundary\mathit{lower1Boundary} (Mutable min-register, initially b+1b+1) \triangleright An integer in {0,,b+1}\{0,\dots,b+1\}
89:     𝑑𝑒𝑙𝑃𝑟𝑒𝑑𝑁𝑜𝑑𝑒\mathit{delPredNode} (Immutable) \triangleright A pointer to a predecessor node
90:     𝑑𝑒𝑙𝑃𝑟𝑒𝑑\mathit{delPred} (Immutable) \triangleright A key in UU
91:     delPred2\mathit{delPred2} (From \bot to a key in UU) \triangleright A key in UU
92:Predecessor Node of P-ALL
93:     𝑘𝑒𝑦\mathit{key} (Immutable) \triangleright A key in UU
94:     𝑛𝑜𝑡𝑖𝑓𝑦𝐿𝑖𝑠𝑡\mathit{notifyList} (Mutable, initially an empty linked list) \triangleright A linked list of notify nodes
95:     𝑅𝑢𝑎𝑙𝑙𝑃𝑜𝑠𝑖𝑡𝑖𝑜𝑛\mathit{RuallPosition} (Mutable, initially a pointer to sentinel update node with key \infty)
96:Notify Node of 𝑝𝑁𝑜𝑑𝑒.𝑛𝑜𝑡𝑖𝑓𝑦𝐿𝑖𝑠𝑡\mathit{pNode}.\mathit{notifyList} for each 𝑝𝑁𝑜𝑑𝑒P-ALL\mathit{pNode}\in\textit{P-ALL}
97:     𝑘𝑒𝑦\mathit{key} (Immutable) \triangleright A key in UU
98:     𝑢𝑝𝑑𝑎𝑡𝑒𝑁𝑜𝑑𝑒\mathit{updateNode} (Immutable) \triangleright A pointer to an update node
99:     𝑢𝑝𝑑𝑎𝑡𝑒𝑁𝑜𝑑𝑒𝑀𝑎𝑥\mathit{updateNodeMax} (Immutable) \triangleright A pointer to an update node
100:     𝑛𝑜𝑡𝑖𝑓𝑦𝑇ℎ𝑟𝑒𝑠ℎ𝑜𝑙𝑑\mathit{notifyThreshold} (Immutable) \triangleright A key in UU
101:Binary Trie Node
102:     𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟\mathit{dNodePtr} (Mutuable, initially points to a dummy DEL node) \triangleright A pointer to a DEL node
Figure 5: Summary of the fields and initial values of each linked list node used by the data structure.

5.2.1 Search Operations

The Search(x)(x) algorithm finds the first activated update node in latest[x]\textit{latest}[x] by calling FindLatest(x)(x). It returns True if this update node has type INS, and False if this update node has type DEL.

The helper function FindLatest(x)(x) first reads the update node \ell pointed to by latest[x].head\textit{latest}[x].head. If \ell is inactive, then the update node, mm, pointed to by its next pointer is read. If mm is \bot, then \ell was activated sometime between when \ell was read to be inactive and when its next pointer was read. If mm is an update node, then mm was active when \ell was read to be inactive. If FindLatest(x)(x) returns an update node 𝑢𝑁𝑜𝑑𝑒\mathit{uNode}, we prove that there is a configuration during the instance of FindLatest in which 𝑢𝑁𝑜𝑑𝑒\mathit{uNode} is the first activated update node in latest[x]\textit{latest}[x].

The helper function FirstActivated(v)(v) takes a pointer vv to an activated update node and checks if vv is the first activated update node in latest[v.key]\textit{latest}[v.key]. If first reads the update node \ell pointed to by latest[x].head\textit{latest}[x].head. If =v\ell=v, then the algorithm returns True because vv is the first activated update node in latest[v.key]\textit{latest}[v.key]. If \ell is inactive and its next pointer points to vv, then vv is the first activated update node in latest[v.key]\textit{latest}[v.key] when \ell was read inactive.

103:Algorithm FindLatest(x)(x)
104:     latest[x].head\ell\leftarrow\textit{latest}[x].head
105:     if (.𝑠𝑡𝑎𝑡𝑢𝑠=Inactive\ell.\mathit{status}=\textsc{Inactive}then
106:         m.𝑙𝑎𝑡𝑒𝑠𝑡𝑁𝑒𝑥𝑡m\leftarrow\ell.\mathit{latestNext}
107:         if mm\neq\bot then return mm               return \ell
108:Algorithm TrieSearch(x)(x)
109:     𝑢𝑁𝑜𝑑𝑒FindLatest(x)\mathit{uNode}\leftarrow\textsc{FindLatest}(x)
110:     if 𝑢𝑁𝑜𝑑𝑒.𝑡𝑦𝑝𝑒=INS\mathit{uNode}.\mathit{type}=\textsc{INS} then return True
111:     else
112:         return False      
113:Algorithm FirstActivated(v)(v)
114:     latest[v.𝑘𝑒𝑦].head\ell\leftarrow\textit{latest}[v.\mathit{key}].head
115:     return v=v=\ell OR (.status=Inactive\ell.status=\textsc{Inactive} AND v=.𝑙𝑎𝑡𝑒𝑠𝑡𝑁𝑒𝑥𝑡v=\ell.\mathit{latestNext})

5.2.2 Insert Operations

We next describe the algorithm for an Insert(x)(x) operation iOpiOp. It is roughly divided into the main parts of inserting a new INS node, 𝑖𝑁𝑜𝑑𝑒\mathit{iNode}, into latest[x]\textit{latest}[x], adding 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} to U-ALL, updating the relaxed binary trie, notifying predecessor operations, and removing 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} from the U-ALL.

It begins by finding the first update node in latest[x]\textit{latest}[x]. If this is an INS node then iOpiOp returns, because xx is already in SS. Otherwise, latest[x]\textit{latest}[x] begins with in a DEL node, 𝑑𝑁𝑜𝑑𝑒\mathit{dNode}. A new update node 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} containing information about iOpiOp is created, and CAS is used to try to change latest[x].ℎ𝑒𝑎𝑑\textit{latest}[x].\mathit{head} to point from 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} to 𝑖𝑁𝑜𝑑𝑒\mathit{iNode}. If the CAS is unsuccessful, then some other Insert(x)(x) operation, iOpiOp^{\prime}, successfully updated latest[x].ℎ𝑒𝑎𝑑\textit{latest}[x].\mathit{head} to point to 𝑖𝑁𝑜𝑑𝑒\mathit{iNode}^{\prime}. In this case, iOpiOp invokes HelpActivate(𝑖𝑁𝑜𝑑𝑒)(\mathit{iNode}^{\prime}) to help activate 𝑖𝑁𝑜𝑑𝑒\mathit{iNode}^{\prime}, and hence linearize iOpiOp^{\prime}. First, iOpiOp helps iOpiOp^{\prime} add 𝑖𝑁𝑜𝑑𝑒\mathit{iNode}^{\prime} to the U-ALL, and changes the status of 𝑖𝑁𝑜𝑑𝑒\mathit{iNode}^{\prime} from Inactive to Active. Then iOpiOp checks if 𝑖𝑁𝑜𝑑𝑒.completed\mathit{iNode}^{\prime}.\textit{completed} is set to True (on line 121). This indicates that iOpiOp^{\prime} has completed updating the relaxed binary trie, and 𝑖𝑁𝑜𝑑𝑒\mathit{iNode}^{\prime} no longer needs to be in U-ALL or RU-ALL. It is possible that iOpiOp^{\prime} already removed 𝑖𝑁𝑜𝑑𝑒\mathit{iNode}^{\prime} from the U-ALL and RU-ALL, but iOpiOp After iOpiOp removes 𝑖𝑁𝑜𝑑𝑒\mathit{iNode}^{\prime} from the U-ALL and RU-ALL, it returns.

Otherwise the CAS is successful, then iOpiOp inserts 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} into the U-ALL and RU-ALL. The status of 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} is changed from Inactive to Active using CAS, which announces the operation. The operation iOpiOp is linearized immediately after the first write that changes 𝑖𝑁𝑜𝑑𝑒.𝑠𝑡𝑎𝑡𝑢𝑠\mathit{iNode}.\mathit{status} from Inactive to Active, which may be performed by iOpiOp or a Insert(x)(x) operation helping iOpiOp.

116:Algorithm HelpActivate(𝑢𝑁𝑜𝑑𝑒)(\mathit{uNode})
117:     if 𝑢𝑁𝑜𝑑𝑒.status=Inactive\mathit{uNode}.status=\textsc{Inactive} then
118:         Insert 𝑢𝑁𝑜𝑑𝑒\mathit{uNode} into U-ALL and RU-ALL
119:         𝑢𝑁𝑜𝑑𝑒.𝑠𝑡𝑎𝑡𝑢𝑠Active\mathit{uNode}.\mathit{status}\leftarrow\textsc{Active}
120:         𝑢𝑁𝑜𝑑𝑒.𝑙𝑎𝑡𝑒𝑠𝑡𝑁𝑒𝑥𝑡\mathit{uNode}.\mathit{latestNext}\leftarrow\bot
121:         if 𝑢𝑁𝑜𝑑𝑒.completed=True\mathit{uNode}.\textit{completed}=\textsc{True} then \triangleright 𝑢𝑁𝑜𝑑𝑒\mathit{uNode} no longer needed in U-ALL or RU-ALL
122:              Remove 𝑢𝑁𝑜𝑑𝑒\mathit{uNode} from U-ALL and RU-ALL               
123:Algorithm TraverseUall(x)(x)
124:     Initialize local variables II\leftarrow\emptyset and DD\leftarrow\emptyset
125:     𝑢𝑁𝑜𝑑𝑒U-ALL.ℎ𝑒𝑎𝑑\mathit{uNode}\leftarrow\textit{U-ALL}.\mathit{head}
126:     while 𝑢𝑁𝑜𝑑𝑒\mathit{uNode}\neq\bot and 𝑢𝑁𝑜𝑑𝑒.𝑘𝑒𝑦<x\mathit{uNode}.\mathit{key}<x do
127:         if (𝑢𝑁𝑜𝑑𝑒.𝑠𝑡𝑎𝑡𝑢𝑠Inactive\mathit{uNode}.\mathit{status}\neq\textsc{Inactive} and FirstActivated(𝑢𝑁𝑜𝑑𝑒)\textsc{FirstActivated}(\mathit{uNode})then
128:              if 𝑢𝑁𝑜𝑑𝑒.type=INS\mathit{uNode}.type=\textsc{INS} then II{𝑢𝑁𝑜𝑑𝑒}I\leftarrow I\cup\{\mathit{uNode}\}
129:              else DD{𝑢𝑁𝑜𝑑𝑒}D\leftarrow D\cup\{\mathit{uNode}\}                        
130:         𝑢𝑁𝑜𝑑𝑒𝑢𝑁𝑜𝑑𝑒.next\mathit{uNode}\leftarrow\mathit{uNode}.next      
131:     return I,DI,D
132:Algorithm NotifyPredOps(𝑢𝑁𝑜𝑑𝑒)(\mathit{uNode})
133:     I,DTraverseUall()I,D\leftarrow\textsc{TraverseUall}(\infty)
134:     for each node 𝑝𝑁𝑜𝑑𝑒P-ALL\mathit{pNode}\in\textit{P-ALL} do
135:         if FirstActivated(𝑢𝑁𝑜𝑑𝑒)\textsc{FirstActivated}(\mathit{uNode}) then
136:              Create a notify node 𝑛𝑁𝑜𝑑𝑒\mathit{nNode}:
137:               𝑛𝑁𝑜𝑑𝑒.𝑘𝑒𝑦𝑢𝑁𝑜𝑑𝑒.𝑘𝑒𝑦\mathit{nNode}.\mathit{key}\leftarrow\mathit{uNode}.\mathit{key}
138:               𝑛𝑁𝑜𝑑𝑒.𝑢𝑝𝑑𝑎𝑡𝑒𝑁𝑜𝑑𝑒𝑢𝑁𝑜𝑑𝑒\mathit{nNode}.\mathit{updateNode}\leftarrow\mathit{uNode}
139:               𝑛𝑁𝑜𝑑𝑒.𝑢𝑝𝑑𝑎𝑡𝑒𝑁𝑜𝑑𝑒𝑀𝑎𝑥\mathit{nNode}.\mathit{updateNodeMax}\leftarrow INS node in II with largest key less than 𝑝𝑁𝑜𝑑𝑒.𝑘𝑒𝑦\mathit{pNode}.\mathit{key}
140:               𝑛𝑁𝑜𝑑𝑒.𝑛𝑜𝑡𝑖𝑓𝑦𝑇ℎ𝑟𝑒𝑠ℎ𝑜𝑙𝑑𝑝𝑁𝑜𝑑𝑒.𝑅𝑢𝑎𝑙𝑙𝑃𝑜𝑠𝑖𝑡𝑖𝑜𝑛.𝑘𝑒𝑦\mathit{nNode}.\mathit{notifyThreshold}\leftarrow\mathit{pNode}.\mathit{RuallPosition}.\mathit{key}
141:              SendNotification(𝑛𝑁𝑜𝑑𝑒,𝑝𝑁𝑜𝑑𝑒)\textsc{SendNotification}(\mathit{nNode},\mathit{pNode})               
142:Algorithm SendNotification(𝑛𝑁𝑜𝑑𝑒𝑁𝑒𝑤,𝑝𝑁𝑜𝑑𝑒)(\mathit{nNodeNew},\mathit{pNode})
143:     while True do
144:         𝑛𝑁𝑜𝑑𝑒𝑝𝑁𝑜𝑑𝑒.𝑛𝑜𝑡𝑖𝑓𝑦𝐿𝑖𝑠𝑡.ℎ𝑒𝑎𝑑\mathit{nNode}\leftarrow\mathit{pNode}.\mathit{notifyList}.\mathit{head}
145:         𝑛𝑁𝑜𝑑𝑒𝑁𝑒𝑤.𝑛𝑒𝑥𝑡𝑛𝑁𝑜𝑑𝑒\mathit{nNodeNew}.\mathit{next}\leftarrow\mathit{nNode}
146:         if FirstActivated(𝑛𝑁𝑜𝑑𝑒𝑁𝑒𝑤.𝑢𝑝𝑑𝑎𝑡𝑒𝑁𝑜𝑑𝑒)=False(\mathit{nNodeNew}.\mathit{updateNode})=\textsc{False} then return          
147:         if CAS(𝑝𝑁𝑜𝑑𝑒.𝑛𝑜𝑡𝑖𝑓𝑦𝐿𝑖𝑠𝑡.ℎ𝑒𝑎𝑑,𝑛𝑁𝑜𝑑𝑒,𝑛𝑁𝑜𝑑𝑒𝑁𝑒𝑤)=True(\mathit{pNode}.\mathit{notifyList}.\mathit{head},\mathit{nNode},\mathit{nNodeNew})=\textsc{True} then return               
148:Algorithm Insert(x)(x)
149:     𝑑𝑁𝑜𝑑𝑒FindLatest(x)\mathit{dNode}\leftarrow\textsc{FindLatest}(x)
150:     if 𝑑𝑁𝑜𝑑𝑒.𝑡𝑦𝑝𝑒DEL\mathit{dNode}.\mathit{type}\neq\text{DEL} then return \triangleright xx is already in SS      
151:     Let 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} be a pointer to a new update node:
152:      𝑖𝑁𝑜𝑑𝑒.𝑘𝑒𝑦x\mathit{iNode}.\mathit{key}\leftarrow x, 𝑖𝑁𝑜𝑑𝑒.𝑡𝑦𝑝𝑒INS\mathit{iNode}.\mathit{type}\leftarrow\text{INS}
153:      𝑖𝑁𝑜𝑑𝑒.𝑙𝑎𝑡𝑒𝑠𝑡𝑁𝑒𝑥𝑡𝑑𝑁𝑜𝑑𝑒\mathit{iNode}.\mathit{latestNext}\leftarrow\mathit{dNode}
154:     𝑑𝑁𝑜𝑑𝑒.𝑙𝑎𝑡𝑒𝑠𝑡𝑁𝑒𝑥𝑡\mathit{dNode}.\mathit{latestNext}\leftarrow\bot
155:     if CAS(latest[x].head,𝑑𝑁𝑜𝑑𝑒,𝑖𝑁𝑜𝑑𝑒)=False(\textit{latest}[x].head,\mathit{dNode},\mathit{iNode})=\textsc{False} then
156:         HelpActivate(latest[x].head)(\textit{latest}[x].head)
157:         return      
158:     Insert 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} into U-ALL and RU-ALL.
159:     𝑖𝑁𝑜𝑑𝑒.𝑠𝑡𝑎𝑡𝑢𝑠Active\mathit{iNode}.\mathit{status}\leftarrow\textsc{Active}
160:     𝑖𝑁𝑜𝑑𝑒.𝑙𝑎𝑡𝑒𝑠𝑡𝑁𝑒𝑥𝑡\mathit{iNode}.\mathit{latestNext}\leftarrow\bot
161:     InsertBinaryTrie(𝑖𝑁𝑜𝑑𝑒)(\mathit{iNode})
162:     NotifyPredOps(𝑖𝑁𝑜𝑑𝑒)\textsc{NotifyPredOps}(\mathit{iNode})
163:     𝑖𝑁𝑜𝑑𝑒.completedTrue\mathit{iNode}.\textit{completed}\leftarrow\textsc{True}
164:     Remove 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} from U-ALL and RU-ALL.
165:     return

The helper function NotifyPredOps(𝑖𝑁𝑜𝑑𝑒)(\mathit{iNode}) sends a notification to all predecessor nodes in P-ALL containing information about 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} as follows. First, iOpiOp invokes TraverseUall(x)(x) (on line 133). It finds update nodes with key less than xx that are the first activated update node in their respective latest lists. INS nodes are put into iOpiOp’s local set II and DEL nodes into iOpiOp’s local set DD. It simply traverses U-ALL, and checking if each update node 𝑢𝑁𝑜𝑑𝑒\mathit{uNode} visited has key less than xx and is the first activated update node in latest[𝑢𝑁𝑜𝑑𝑒.𝑘𝑒𝑦]\textit{latest}[\mathit{uNode}.\mathit{key}]. If so, 𝑢𝑁𝑜𝑑𝑒\mathit{uNode} is added to II if it is an INS node, or it is added to DD if it is a DEL node. We prove that the keys of the nodes in II are in the set SS sometime during the traversal, while the keys in DD are not in the set SS sometime during the traversal.

Then iOpiOp notifies each predecessor node 𝑝𝑁𝑜𝑑𝑒\mathit{pNode} in P-ALL with 𝑝𝑁𝑜𝑑𝑒.𝑘𝑒𝑦>x\mathit{pNode}.\mathit{key}>x. It creates a new notify node, 𝑛𝑁𝑜𝑑𝑒\mathit{nNode}, containing a pointer to iOpiOp’s update node and a pointer to the INS node in II with the largest key less than 𝑝𝑁𝑜𝑑𝑒.𝑘𝑒𝑦\mathit{pNode}.\mathit{key}. It also reads the value stored in 𝑝𝑁𝑜𝑑𝑒.𝑅𝑢𝑎𝑙𝑙𝑃𝑜𝑠𝑖𝑡𝑖𝑜𝑛\mathit{pNode}.\mathit{RuallPosition}, which is a pointer to an update node. The key of this update node is written into 𝑛𝑁𝑜𝑑𝑒.𝑛𝑜𝑡𝑖𝑓𝑦𝑇ℎ𝑟𝑒𝑠ℎ𝑜𝑙𝑑\mathit{nNode}.\mathit{notifyThreshold} (on line 140). This information is used by the predecessor operation to determine if the notification sent by iOpiOp should be used to determine a candidate return value.

Before returning, the update node 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} is removed from the U-ALL and RU-ALL.

5.2.3 Delete Operations

166:Algorithm Delete(x)(x)
167:     𝑖𝑁𝑜𝑑𝑒FindLatest(x)\mathit{iNode}\leftarrow\textsc{FindLatest}(x)
168:     if 𝑖𝑁𝑜𝑑𝑒.𝑡𝑦𝑝𝑒INS\mathit{iNode}.\mathit{type}\neq\text{INS} then return \triangleright xx is not in SS      
169:     𝑑𝑒𝑙𝑃𝑟𝑒𝑑,𝑝𝑁𝑜𝑑𝑒1PredHelper(x)\mathit{delPred},\mathit{pNode}1\leftarrow\textsc{PredHelper}(x)
170:     Let 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} be a pointer to a new update node:
171:      𝑑𝑁𝑜𝑑𝑒.𝑘𝑒𝑦x\mathit{dNode}.\mathit{key}\leftarrow x, 𝑑𝑁𝑜𝑑𝑒.𝑡𝑦𝑝𝑒DEL\mathit{dNode}.\mathit{type}\leftarrow\text{DEL}
172:      𝑑𝑁𝑜𝑑𝑒.𝑙𝑎𝑡𝑒𝑠𝑡𝑁𝑒𝑥𝑡𝑖𝑁𝑜𝑑𝑒\mathit{dNode}.\mathit{latestNext}\leftarrow\mathit{iNode}
173:      𝑑𝑁𝑜𝑑𝑒𝑑𝑒𝑙𝑃𝑟𝑒𝑑𝑑𝑒𝑙𝑃𝑟𝑒𝑑\mathit{dNode}\mathit{delPred}\leftarrow\mathit{delPred}
174:      𝑑𝑁𝑜𝑑𝑒.𝑑𝑒𝑙𝑃𝑟𝑒𝑑𝑁𝑜𝑑𝑒𝑝𝑁𝑜𝑑𝑒1\mathit{dNode}.\mathit{delPredNode}\leftarrow\mathit{pNode}1
175:     𝑖𝑁𝑜𝑑𝑒.𝑙𝑎𝑡𝑒𝑠𝑡𝑁𝑒𝑥𝑡\mathit{iNode}.\mathit{latestNext}\leftarrow\bot
176:     NotifyPredOps(𝑖𝑁𝑜𝑑𝑒)(\mathit{iNode})\triangleright Help previous Insert send notifications
177:     if CAS(latest[x].head,𝑖𝑁𝑜𝑑𝑒,𝑑𝑁𝑜𝑑𝑒)=False(\textit{latest}[x].head,\mathit{iNode},\mathit{dNode})=\textsc{False} then
178:         HelpActivate(latest[x].head)(\textit{latest}[x].head)
179:         Delete 𝑝𝑁𝑜𝑑𝑒1\mathit{pNode}1 from P-ALL.
180:         return      
181:     Insert 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} into U-ALL and RU-ALL
182:     𝑑𝑁𝑜𝑑𝑒.𝑠𝑡𝑎𝑡𝑢𝑠Active\mathit{dNode}.\mathit{status}\leftarrow\textsc{Active}
183:     𝑖𝑁𝑜𝑑𝑒.target.stopTrue\mathit{iNode}.\textit{target}.\textit{stop}\leftarrow\textsc{True}
184:     𝑑𝑁𝑜𝑑𝑒.𝑙𝑎𝑡𝑒𝑠𝑡𝑁𝑒𝑥𝑡\mathit{dNode}.\mathit{latestNext}\leftarrow\bot
185:     delPred2,𝑝𝑁𝑜𝑑𝑒2PredHelper(x)\mathit{delPred2},\mathit{pNode}2\leftarrow\textsc{PredHelper}(x)
186:     𝑑𝑁𝑜𝑑𝑒.delPred2delPred2\mathit{dNode}.\mathit{delPred2}\leftarrow\mathit{delPred2}
187:     DeleteBinaryTrie(𝑑𝑁𝑜𝑑𝑒)(\mathit{dNode})
188:     NotifyPredOps(𝑑𝑁𝑜𝑑𝑒)(\mathit{dNode})
189:     𝑑𝑁𝑜𝑑𝑒.completedTrue\mathit{dNode}.\textit{completed}\leftarrow\textsc{True}
190:     Delete 𝑝𝑁𝑜𝑑𝑒1\mathit{pNode}1 and 𝑝𝑁𝑜𝑑𝑒2\mathit{pNode}2 from P-ALL.
191:     Delete 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} from U-ALL and RU-ALL

We next describe the algorithm for a Delete(x)(x) operation, dOpdOp. The algorithm is similar to an Insert(x)(x) operation, but with a few more parts. Most importantly, dOpdOp may perform embedded predecessor operations, which we describe later. The main parts of a Delete(x)(x) operation are performing an embedded predecessor, notifying Predecessor operations about the previous SS-modifying Insert(x)(x) operation, notifying predecessor operations, inserting a new DEL node into latest[x]\textit{latest}[x] and then into the U-ALL, performing a second embedded predecessor operation, updating the relaxed binary trie, and then notifying Predecessor operations about its own operation.

The Delete(x)(x) operation, dOpdOp, begins by finding the first activated update node in latest[x]\textit{latest}[x]. It immediately returns if this is a DEL node, since xSx\notin S. Otherwise, latest[x]\textit{latest}[x] begins with a INS node, 𝑖𝑁𝑜𝑑𝑒\mathit{iNode}. Next, dOpdOp performs an embedded predecessor operation with key xx. The result of the embedded predecessor is saved in a new DEL node 𝑑𝑁𝑜𝑑𝑒\mathit{dNode}, along with other information. Recall that this embedded predecessor will be used by concurrent Predecessor operations in the case that dOpdOp prevents them from traversing the relaxed binary trie.

Additionally, dOpdOp performs NotifyPredOpds(𝑖𝑁𝑜𝑑𝑒)(\mathit{iNode}) to help notify 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} notify predecessor operations. Since an Insert(x)(x) operation does not send notifications when its update node is no longer the first activated update node in latest[x]\textit{latest}[x], we ensure at least one update operation with key xx notifies all predecessor nodes about 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} before a Delete(x)(x) operation is linearized.

Then dOpdOp attempts to add 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} to the head of latest[x]\textit{latest}[x], by using CAS to change latest[x].ℎ𝑒𝑎𝑑\textit{latest}[x].\mathit{head} to point from 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} to 𝑑𝑁𝑜𝑑𝑒\mathit{dNode}. If the CAS is unsuccessful, a concurrent Delete(x)(x) operation, dOpdOp^{\prime}, successfully updated latest[x].ℎ𝑒𝑎𝑑\textit{latest}[x].\mathit{head} to point to some update node 𝑑𝑁𝑜𝑑𝑒\mathit{dNode}^{\prime}. So dOpdOp performs HelpActivate(𝑑𝑁𝑜𝑑𝑒)(\mathit{dNode}^{\prime}) to help dOpdOp^{\prime} linearize and then dOpdOp returns. If the CAS is successful, dOpdOp inserts 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} into the U-ALL and RU-ALL and changes 𝑑𝑁𝑜𝑑𝑒.𝑠𝑡𝑎𝑡𝑢𝑠\mathit{dNode}.\mathit{status} from Inactive to Active using a write. The linearization point of dOpdOp is immediately after the status of 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} first changes from Inactive to Active. Next, dOpdOp performs a second embedded predecessor operation and records the result in 𝑑𝑁𝑜𝑑𝑒\mathit{dNode}.

The binary trie is updated in DeleteBinaryTrie(x)(x). It proceeds similarly to the sequential binary trie delete algorithm, traversing a path through the binary trie starting from the leaf with key xx. Let tt be an internal binary trie node on the path from the leaf with key xx to the root. Suppose dOpdOp successfully changed the interpreted bit of one of tt’s children to 0. If the interpreted bit of the other child of tt is 0, then dOpdOp attempts to change the interpreted bit of tt to 0. Recall that tt depends on the first activated update node in latest[t.𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟.𝑘𝑒𝑦]\textit{latest}[t.\mathit{dNodePtr}.\mathit{key}]. To change tt to depend on 𝑑𝑁𝑜𝑑𝑒\mathit{dNode}, dOpdOp performs CAS to attempt to change t.𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟t.\mathit{dNodePtr} to point to 𝑑𝑁𝑜𝑑𝑒\mathit{dNode}. Note that dOpdOp performs two attempts of this CAS, each time checking its 𝑑𝑁𝑜𝑑𝑒.stop\mathit{dNode}.\textit{stop} is not set to True (indicating a concurrent Insert(x)(x) wants to set the interpreted bit of tt to 1) and that 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} is still the first activated update node in latest[x]\textit{latest}[x]. Two CAS attempts are performed to prevent out-dated Delete operations that were poised to perform CAS from conflicting with latest Delete operations. If dOpdOp is unsuccessful in both its CAS attempts, it can stop updating the binary trie because some concurrent Delete(x)(x^{\prime}) operation, with key xUtx^{\prime}\in U_{t}, successfully changed t.𝑑𝑁𝑜𝑑𝑒𝑃𝑡𝑟t.\mathit{dNodePtr} to point to its own DEL node. Otherwise dOpdOp is successful in changing the interpreted bit of tt to depend on 𝑑𝑁𝑜𝑑𝑒\mathit{dNode}. Immediately after dOpdOp’s successful CAS, the interpreted bit of tt is still 1 (because 𝑑𝑁𝑜𝑑𝑒.upper0Boundary\mathit{dNode}.\mathit{upper0Boundary} has not yet been incremented to t.0ptt.0pt). Once again, dOpdOp verifies both children of tt have interpreted bit 0, otherwise it returns. To change the interpreted bit of 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} to 0, dOpdOp writes t.0ptt.0pt into 𝑑𝑁𝑜𝑑𝑒.upper0Boundary\mathit{dNode}.\mathit{upper0Boundary}, which increments its value. This indicates that all binary trie nodes at height tt and below that depend on 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} have interpreted bit 0. Only dOpdOp, the creator of 𝑑𝑁𝑜𝑑𝑒\mathit{dNode}, writes to 𝑑𝑁𝑜𝑑𝑒.upper0Boundary\mathit{dNode}.\mathit{upper0Boundary}. Since dOpdOp changes the interpreted bits of binary trie nodes in order from the leaf with key xx to the root, 𝑑𝑁𝑜𝑑𝑒.upper0Boundary\mathit{dNode}.\mathit{upper0Boundary} is only ever incremented by 1 starting from 0.

Once the relaxed binary trie has been updated, dOpdOp notifies predecessor operations using NotifyPredOps(𝑑𝑁𝑜𝑑𝑒)(\mathit{dNode}) described previously. Finally, dOpdOp deletes the predecessor nodes it created for its embedded predecessor operations from P-ALL and removes its update node 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} from the U-ALL and RU-ALL.

5.2.4 Predecessor Operations

A Predecessor(y)(y) operation begins by calling an instance, pOppOp, of PredHelper(y)\textsc{PredHelper}(y), which does all the steps of the operation except for removing the announcement from the P-ALL. This helper function is also used by Delete(y)(y) operations to perform their embedded predecessor operations. Recall that these embedded predecessor operations do not remove their announcements from the P-ALL until the end of their Delete(y)(y) operation. The helper function PredHelper(y)\textsc{PredHelper}(y) is complicated, so its description is divided into six main parts: announcing the operation in the P-ALL, traversing the RU-ALL, traversing the relaxed binary trie, traversing the U-ALL, collecting notifications, and handling the case when the traversal the relaxed binary trie returns \bot. These parts are performed one after the other in this order.

192:Algorithm PredHelper(y)(y)
193:     Create predecessor node 𝑛𝑒𝑤𝑁𝑜𝑑𝑒\mathit{newNode} with key yy
194:     Insert 𝑝𝑁𝑜𝑑𝑒\mathit{pNode} to the head of P-ALL
195:     𝑝𝑁𝑜𝑑𝑒𝑝𝑁𝑜𝑑𝑒.𝑛𝑒𝑥𝑡\mathit{pNode}^{\prime}\leftarrow\mathit{pNode}.\mathit{next}
196:     Q()Q\leftarrow() \triangleright Initialize local sequence
197:     while 𝑝𝑁𝑜𝑑𝑒\mathit{pNode}^{\prime}\neq\bot do
198:         prepend 𝑝𝑁𝑜𝑑𝑒\mathit{pNode}^{\prime} to QQ
199:         𝑝𝑁𝑜𝑑𝑒𝑝𝑁𝑜𝑑𝑒.𝑛𝑒𝑥𝑡\mathit{pNode}^{\prime}\leftarrow\mathit{pNode}^{\prime}.\mathit{next}      
200:     \triangleright Determine active Delete operations at the start of Pred operation
201:     (I𝑟𝑢𝑎𝑙𝑙,D𝑟𝑢𝑎𝑙𝑙)TraverseRUall(𝑝𝑁𝑜𝑑𝑒)(I_{\mathit{ruall}},D_{\mathit{ruall}})\leftarrow\textsc{TraverseRUall}(\mathit{pNode})
202:     p0RelaxedPredecessor(y)p_{0}\leftarrow\textsc{RelaxedPredecessor}(y)
203:     (I𝑢𝑎𝑙𝑙,D𝑢𝑎𝑙𝑙)TraverseUall(y)(I_{\mathit{uall}},D_{\mathit{uall}})\leftarrow\textsc{TraverseUall}(y)
204:     (I𝑛𝑜𝑡𝑖𝑓𝑦,D𝑛𝑜𝑡𝑖𝑓𝑦)(,)(I_{\mathit{notify}},D_{\mathit{notify}})\leftarrow(\emptyset,\emptyset)
205:     for each notify node 𝑛𝑁𝑜𝑑𝑒\mathit{nNode} in 𝑝𝑁𝑜𝑑𝑒.𝑛𝑜𝑡𝑖𝑓𝑦𝐿𝑖𝑠𝑡\mathit{pNode}.\mathit{notifyList} with key less than yy do
206:         if 𝑛𝑁𝑜𝑑𝑒.𝑢𝑝𝑑𝑎𝑡𝑒𝑁𝑜𝑑𝑒.𝑡𝑦𝑝𝑒=INS\mathit{nNode}.\mathit{updateNode}.\mathit{type}=\text{INS} then
207:              if 𝑛𝑁𝑜𝑑𝑒.𝑛𝑜𝑡𝑖𝑓𝑦𝑇ℎ𝑟𝑒𝑠ℎ𝑜𝑙𝑑𝑛𝑁𝑜𝑑𝑒.𝑘𝑒𝑦\mathit{nNode}.\mathit{notifyThreshold}\leq\mathit{nNode}.\mathit{key} then
208:                  I𝑛𝑜𝑡𝑖𝑓𝑦I𝑛𝑜𝑡𝑖𝑓𝑦{𝑛𝑁𝑜𝑑𝑒.𝑢𝑝𝑑𝑎𝑡𝑒𝑁𝑜𝑑𝑒}I_{\mathit{notify}}\leftarrow I_{\mathit{notify}}\cup\{\mathit{nNode}.\mathit{updateNode}\}               
209:         else
210:              if 𝑛𝑁𝑜𝑑𝑒.𝑛𝑜𝑡𝑖𝑓𝑦𝑇ℎ𝑟𝑒𝑠ℎ𝑜𝑙𝑑<𝑛𝑁𝑜𝑑𝑒.𝑘𝑒𝑦\mathit{nNode}.\mathit{notifyThreshold}<\mathit{nNode}.\mathit{key} then
211:                  D𝑛𝑜𝑡𝑖𝑓𝑦D𝑛𝑜𝑡𝑖𝑓𝑦{𝑛𝑁𝑜𝑑𝑒.𝑢𝑝𝑑𝑎𝑡𝑒𝑁𝑜𝑑𝑒}D_{\mathit{notify}}\leftarrow D_{\mathit{notify}}\cup\{\mathit{nNode}.\mathit{updateNode}\}                        
212:         if 𝑛𝑁𝑜𝑑𝑒.𝑛𝑜𝑡𝑖𝑓𝑦𝑇ℎ𝑟𝑒𝑠ℎ𝑜𝑙𝑑=\mathit{nNode}.\mathit{notifyThreshold}=-\infty and 𝑛𝑁𝑜𝑑𝑒.𝑢𝑝𝑑𝑎𝑡𝑒𝑁𝑜𝑑𝑒(I𝑟𝑢𝑎𝑙𝑙D𝑟𝑢𝑎𝑙𝑙)\mathit{nNode}.\mathit{updateNode}\notin(I_{\mathit{ruall}}\cup D_{\mathit{ruall}}) then
213:              I𝑛𝑜𝑡𝑖𝑓𝑦I𝑛𝑜𝑡𝑖𝑓𝑦{𝑛𝑁𝑜𝑑𝑒.𝑢𝑝𝑑𝑎𝑡𝑒𝑁𝑜𝑑𝑒𝑀𝑎𝑥}I_{\mathit{notify}}\leftarrow I_{\mathit{notify}}\cup\{\mathit{nNode}.\mathit{updateNodeMax}\}               
214:     Kkeys of the update nodes in I𝑢𝑎𝑙𝑙I𝑛𝑜𝑡𝑖𝑓𝑦(D𝑢𝑎𝑙𝑙D𝑟𝑢𝑎𝑙𝑙)(D𝑛𝑜𝑡𝑖𝑓𝑦D𝑟𝑢𝑎𝑙𝑙)K\leftarrow\text{keys of the update nodes in }I_{\mathit{uall}}\cup I_{\mathit{notify}}\cup(D_{\mathit{uall}}-D_{\mathit{ruall}})\cup(D_{\mathit{notify}}-D_{\mathit{ruall}})
215:     p1max{K,1}p_{1}\leftarrow\max\{K,-1\}
216:     \triangleright Unsuccessful traversal of relaxed binary trie
217:     if p0=p_{0}=\bot and D𝑟𝑢𝑎𝑙𝑙D_{\mathit{ruall}}\neq\emptyset then
218:         L1()L_{1}\leftarrow() \triangleright Initialize empty sequence
219:         𝑝𝑟𝑒𝑑𝑁𝑜𝑑𝑒𝑠{𝑝𝑁𝑜𝑑𝑒𝑑𝑁𝑜𝑑𝑒D𝑟𝑢𝑎𝑙𝑙 where 𝑑𝑁𝑜𝑑𝑒.𝑑𝑒𝑙𝑃𝑟𝑒𝑑𝑁𝑜𝑑𝑒=𝑝𝑁𝑜𝑑𝑒}\mathit{predNodes}\leftarrow\{\mathit{pNode}^{\prime}\mid\exists\mathit{dNode}\in D_{\mathit{ruall}}\text{ where }\mathit{dNode}.\mathit{delPredNode}=\mathit{pNode}^{\prime}\}
220:         if QQ contains a predecessor node in 𝑝𝑟𝑒𝑑𝑁𝑜𝑑𝑒𝑠\mathit{predNodes} then
221:              𝑝𝑁𝑜𝑑𝑒\mathit{pNode}^{\prime}\leftarrow predecessor node in 𝑝𝑟𝑒𝑑𝑁𝑜𝑑𝑒𝑠\mathit{predNodes} that occurs earliest in QQ
222:              for each notify node 𝑛𝑁𝑜𝑑𝑒\mathit{nNode} in 𝑝𝑁𝑜𝑑𝑒.𝑛𝑜𝑡𝑖𝑓𝑦𝐿𝑖𝑠𝑡\mathit{pNode^{\prime}}.\mathit{notifyList} with key less than yy do
223:                  prepend 𝑛𝑁𝑜𝑑𝑒.𝑢𝑝𝑑𝑎𝑡𝑒𝑁𝑜𝑑𝑒\mathit{nNode}.\mathit{updateNode} to L1L_{1}                        
224:         L{}L^{\prime}\leftarrow\{\}, L2()L_{2}\leftarrow() \triangleright Initialize empty set and sequence
225:         for each notify node 𝑛𝑁𝑜𝑑𝑒\mathit{nNode} in 𝑝𝑁𝑜𝑑𝑒.𝑛𝑜𝑡𝑖𝑓𝑦𝐿𝑖𝑠𝑡\mathit{pNode}.\mathit{notifyList} with key less than yy do
226:              if 𝑛𝑁𝑜𝑑𝑒.𝑛𝑜𝑡𝑖𝑓𝑦𝑇ℎ𝑟𝑒𝑠ℎ𝑜𝑙𝑑𝑛𝑁𝑜𝑑𝑒.𝑘𝑒𝑦\mathit{nNode}.\mathit{notifyThreshold}\geq\mathit{nNode}.\mathit{key} then
227:                  prepend 𝑛𝑁𝑜𝑑𝑒.𝑢𝑝𝑑𝑎𝑡𝑒𝑁𝑜𝑑𝑒\mathit{nNode}.\mathit{updateNode} to L2L_{2}
228:              else
229:                  LL{𝑛𝑁𝑜𝑑𝑒.𝑢𝑝𝑑𝑎𝑡𝑒𝑁𝑜𝑑𝑒}L^{\prime}\leftarrow L^{\prime}\cup\{\mathit{nNode}.\mathit{updateNode}\}                        
230:         LL\leftarrow sequence of update nodes in L1L_{1} followed by L2L_{2} that are not in LL^{\prime}
231:         RR{w𝑑𝑁𝑜𝑑𝑒D𝑟𝑢𝑎𝑙𝑙 where 𝑑𝑁𝑜𝑑𝑒.𝑑𝑒𝑙𝑃𝑟𝑒𝑑=w}R\leftarrow R\cup\{w\mid\exists\mathit{dNode}\in D_{\mathit{ruall}}\text{ where }\mathit{dNode}.\mathit{delPred}=w\}
232:         for each update node 𝑢𝑁𝑜𝑑𝑒\mathit{uNode} in LL in order do
233:              if 𝑢𝑁𝑜𝑑𝑒.𝑡𝑦𝑝𝑒=INS\mathit{uNode}.\mathit{type}=\text{INS} then RR{𝑢𝑁𝑜𝑑𝑒.𝑘𝑒𝑦}R\leftarrow R\cup\{\mathit{uNode}.\mathit{key}\}
234:              else
235:                  if  𝑢𝑁𝑜𝑑𝑒.𝑘𝑒𝑦R\mathit{uNode}.\mathit{key}\in R  then RR{𝑢𝑁𝑜𝑑𝑒.𝑘𝑒𝑦}{𝑢𝑁𝑜𝑑𝑒.delPred2}R\leftarrow R-\{\mathit{uNode}.\mathit{key}\}\cup\{\mathit{uNode}.\mathit{delPred2}\}                                          
236:         RR{wR𝑑𝑁𝑜𝑑𝑒D𝑟𝑢𝑎𝑙𝑙 where 𝑑𝑁𝑜𝑑𝑒.𝑘𝑒𝑦=w}R\leftarrow R-\{w\in R\mid\exists\mathit{dNode}\in D_{\mathit{ruall}}\text{ where }\mathit{dNode}.\mathit{key}=w\}
237:         p0max{R}p_{0}\leftarrow\max\{R\}      
238:     return max{p0,p1}\max\{p_{0},p_{1}\}, 𝑝𝑁𝑜𝑑𝑒\mathit{pNode}
239:Algorithm Predecessor(y)(y)
240:     𝑝𝑟𝑒𝑑,𝑝𝑁𝑜𝑑𝑒PredHelper(y)\mathit{pred},\mathit{pNode}\leftarrow\textsc{PredHelper}(y)
241:     Remove 𝑝𝑁𝑜𝑑𝑒\mathit{pNode} from P-ALL
242:     return 𝑝𝑟𝑒𝑑\mathit{pred}

Announcing the operation in the P-ALL: An instance, pOppOp, of PredHelper(y)(y) announces itself by creating a new predecessor node, 𝑝𝑁𝑜𝑑𝑒\mathit{pNode}, with key yy and inserts it at the head of P-ALL (on line 194). Then it traverses the P-ALL starting from 𝑝𝑁𝑜𝑑𝑒\mathit{pNode} (on line 195 to 199), locally storing the sequence of predecessor nodes it encounters into a sequence QQ.


Traversing the RU-ALL: The traversal of the RU-ALL is done by a call to TraverseRUall(𝑝𝑁𝑜𝑑𝑒)(\mathit{pNode}). During this traversal, pOppOp identifies each update node with key less than yy that is the first activated update node in its latest list. Those with type INS are put into pOppOp’s local set I𝑟𝑢𝑎𝑙𝑙I_{\mathit{ruall}}, while those with type DEL are put into pOppOp’s local set D𝑟𝑢𝑎𝑙𝑙D_{\mathit{ruall}}. The sets I𝑟𝑢𝑎𝑙𝑙I_{\mathit{ruall}} and D𝑟𝑢𝑎𝑙𝑙D_{\mathit{ruall}} include the update nodes of all SS-modifying update operations linearized before the start of pOppOp and are still active at the start of pOppOp’s traversal of the relaxed binary trie. They may additionally contain update nodes of update operations linearized shortly after the start of pOppOp, because it is difficult to distinguish them from those that were linearized before the start of pOppOp. Since the Delete operations of DEL nodes in D𝑟𝑢𝑎𝑙𝑙D_{\mathit{ruall}} may be linearized before the start of pOppOp, they are not used to determine candidate return values. Instead, they are used to eliminate announcements or notifications later seen by pOppOp when it traverses the U-ALL or 𝑝𝑁𝑜𝑑𝑒.𝑛𝑜𝑡𝑖𝑓𝑦𝐿𝑖𝑠𝑡\mathit{pNode}.\mathit{notifyList}.

While pOppOp is traversing the RU-ALL, it makes available the key of the update node it is currently visiting in the RU-ALL. This is done by maintaining 𝑝𝑁𝑜𝑑𝑒.𝑅𝑢𝑎𝑙𝑙𝑃𝑜𝑠𝑖𝑡𝑖𝑜𝑛\mathit{pNode}.\mathit{RuallPosition}, which contains a pointer to the update node in the RU-ALL that pOppOp is currently visiting. Recall that the key field of an update node is immutable, so a pointer to this node is sufficient to obtain its key. Initially, 𝑝𝑁𝑜𝑑𝑒.𝑅𝑢𝑎𝑙𝑙𝑃𝑜𝑠𝑖𝑡𝑖𝑜𝑛\mathit{pNode}.\mathit{RuallPosition} points to the sentinel at the head of the RU-ALL, which has key \infty. Only pOppOp modifies 𝑝𝑁𝑜𝑑𝑒.𝑅𝑢𝑎𝑙𝑙𝑃𝑜𝑠𝑖𝑡𝑖𝑜𝑛\mathit{pNode}.\mathit{RuallPosition}. Each time pOppOp reads a pointer to the next node in the RU-ALL, pOppOp atomically copies this pointer into 𝑝𝑁𝑜𝑑𝑒.𝑅𝑢𝑎𝑙𝑙𝑃𝑜𝑠𝑖𝑡𝑖𝑜𝑛\mathit{pNode}.\mathit{RuallPosition}. Single-writer atomic copy can be implemented from CAS with O(1)O(1) worst-case step complexity [4].

The pseudocode for TraverseRUall(𝑝𝑁𝑜𝑑𝑒)(\mathit{pNode}) is as follows. The local variable 𝑢𝑁𝑜𝑑𝑒\mathit{uNode} is initialized to point to the sentinel node with key \infty at the head of the RU-ALL (on line 246). Then pOppOp traverses the RU-ALL one update node at a time, atomically copying 𝑢𝑁𝑜𝑑𝑒.𝑛𝑒𝑥𝑡\mathit{uNode}.\mathit{next} into 𝑝𝑁𝑜𝑑𝑒.𝑅𝑢𝑎𝑙𝑙𝑃𝑜𝑠𝑖𝑡𝑖𝑜𝑛\mathit{pNode}.\mathit{RuallPosition} (on line 248) before progressing to the next node. It traverses the list until it first reaches an update node with key less than yy. From this point on, it checks whether the update node it is pointing to is the first activated update node in latest[𝑢𝑁𝑜𝑑𝑒.𝑘𝑒𝑦]\textit{latest}[\mathit{uNode}.\mathit{key}] (on line 251). If so, the update node is added to I𝑟𝑢𝑎𝑙𝑙I_{\mathit{ruall}} or D𝑟𝑢𝑎𝑙𝑙D_{\mathit{ruall}}, depending on its type. When 𝑢𝑁𝑜𝑑𝑒.𝑘𝑒𝑦=\mathit{uNode}.\mathit{key}=-\infty, 𝑝𝑁𝑜𝑑𝑒.𝑅𝑢𝑎𝑙𝑙𝑃𝑜𝑠𝑖𝑡𝑖𝑜𝑛\mathit{pNode}.\mathit{RuallPosition} points to the sentinel node at the end of the RU-ALL.

243:Algorithm TraverseRUall(𝑝𝑁𝑜𝑑𝑒)(\mathit{pNode})
244:     Initialize local variables II\leftarrow\emptyset and DD\leftarrow\emptyset
245:     y𝑝𝑁𝑜𝑑𝑒.𝑘𝑒𝑦y\leftarrow\mathit{pNode}.\mathit{key}
246:     𝑢𝑁𝑜𝑑𝑒𝑝𝑁𝑜𝑑𝑒.𝑅𝑢𝑎𝑙𝑙𝑃𝑜𝑠𝑖𝑡𝑖𝑜𝑛\mathit{uNode}\leftarrow\mathit{pNode}.\mathit{RuallPosition}
247:     do
248:         atomic copy 𝑢𝑁𝑜𝑑𝑒.𝑛𝑒𝑥𝑡\mathit{uNode}.\mathit{next} to 𝑝𝑁𝑜𝑑𝑒.𝑅𝑢𝑎𝑙𝑙𝑃𝑜𝑠𝑖𝑡𝑖𝑜𝑛\mathit{pNode}.\mathit{RuallPosition} \triangleright Atomic read and write
249:         𝑢𝑁𝑜𝑑𝑒𝑝𝑁𝑜𝑑𝑒.𝑅𝑢𝑎𝑙𝑙𝑃𝑜𝑠𝑖𝑡𝑖𝑜𝑛\mathit{uNode}\leftarrow\mathit{pNode}.\mathit{RuallPosition}
250:         if 𝑢𝑁𝑜𝑑𝑒.𝑘𝑒𝑦<y\mathit{uNode}.\mathit{key}<y then
251:              if (𝑢𝑁𝑜𝑑𝑒.𝑠𝑡𝑎𝑡𝑢𝑠Inactive\mathit{uNode}.\mathit{status}\neq\textsc{Inactive} and FirstActivated(𝑢𝑁𝑜𝑑𝑒)\textsc{FirstActivated}(\mathit{uNode})then
252:                  if 𝑢𝑁𝑜𝑑𝑒.type=INS\mathit{uNode}.type=\textsc{INS} then II{𝑢𝑁𝑜𝑑𝑒}I\leftarrow I\cup\{\mathit{uNode}\}
253:                  else DD{𝑢𝑁𝑜𝑑𝑒}D\leftarrow D\cup\{\mathit{uNode}\}                                          
254:     while (𝑢𝑁𝑜𝑑𝑒.𝑘𝑒𝑦)(\mathit{uNode}.\mathit{key}\neq-\infty)
255:     return I,DI,D

We now explain the purpose of having available the key of the update node pOppOp is currently visiting in the RU-ALL. Recall that when an update operation creates a notify node, 𝑛𝑁𝑜𝑑𝑒\mathit{nNode}, to add to 𝑝𝑁𝑜𝑑𝑒.𝑛𝑜𝑡𝑖𝑓𝑦𝐿𝑖𝑠𝑡\mathit{pNode}.\mathit{notifyList}, it reads 𝑝𝑁𝑜𝑑𝑒.𝑅𝑢𝑎𝑙𝑙𝑃𝑜𝑠𝑖𝑡𝑖𝑜𝑛.𝑘𝑒𝑦\mathit{pNode}.\mathit{RuallPosition}.\mathit{key} and writes it into 𝑛𝑁𝑜𝑑𝑒.𝑛𝑜𝑡𝑖𝑓𝑦𝑇ℎ𝑟𝑒𝑠ℎ𝑜𝑙𝑑\mathit{nNode}.\mathit{notifyThreshold}. This is used by pOppOp to determine if 𝑛𝑁𝑜𝑑𝑒.𝑘𝑒𝑦\mathit{nNode}.\mathit{key} should be used as a candidate return value. For example, consider a Delete(w)(w) operation, for some where w<yw<y, linearized before the start of pOppOp, that notifies pOppOp. This Delete(w)(w) operation’s DEL node should not be used to determine a candidate return value, because ww was removed from SS before the start of pOppOp. If pOppOp sees this DEL node when it traverses the RU-ALL, then it is added to D𝑟𝑢𝑎𝑙𝑙D_{\mathit{ruall}} and hence will not be used. Otherwise this DEL is removed from the RU-ALL before pOppOp can see it during its traversal of the RU-ALL. The notification from a Delete operation should only be used to determine a candidate return value if pOppOp can guarantee that the Delete operation was linearized sometime during pOppOp. In particular, when pOppOp does not add a DEL node into D𝑟𝑢𝑎𝑙𝑙D_{\mathit{ruall}} and pOppOp is currently at an update node with key strictly less than ww, the Delete(w)(w) operation must have added its DEL node into the RU-ALL before this update node. So only after pOppOp has encountered an update node with key less than ww during its traversal of the RU-ALL does pOppOp begin accepting the notifications of Delete(w)(w) operations. Note that pOppOp cannot accept notifications from Insert(w)(w) operations until pOppOp also accepts notifications from Delete operations with key larger than ww. Otherwise pOppOp may miss a candidate return value larger than ww. So only after pOppOp has encountered an update node with key less than or equal to ww during its traversal of the RU-ALL does pOppOp begin accepting the notifications of Insert(w)(w) operations.

It is important that the RU-ALL is sorted by decreasing key. Since the RU-ALL is sorted by decreasing key, as pOppOp traverses the RU-ALL, it begins accepting notifications from update operations with progressively smaller keys. This is so that if pOppOp determines accepts the notification from an update operation, it does not miss notifications of update operations with larger key. For example, consider the execution depicted in Figure 6.

Refer to caption
Figure 6: An example execution of a Delete(x)(x), Delete(w)(w), and Predecessor(y)(y) operation, where w<x<yw<x<y.

There are three concurrent operations: a Predecessor(y)(y) operation, a Delete(x)(x) operation, and a Delete(w)(w) operation, where w<x<yw<x<y. The Delete(x)(x) operation is linearized after the Delete(w)(w) operation, and both are linearized after the start of the Predecessor(y)(y) operation, pOppOp. Notice that xSx\in S for all configurations during pOppOp in which wSw\in S. So if ww is a candidate return value of pOppOp, pOppOp must also determine a candidate return value which is at least xx. Hence, If pOppOp accepts notifications from Delete(w)(w) operations, it also accepts notifications from update operations with keys larger than ww, and hence xx.

Atomic copy is used to make sure that no update operations modify the next pointer of an update node in the RU-ALL between when pOppOp reads this next pointer and when pOppOp writes a copy it into 𝑝𝑁𝑜𝑑𝑒.𝑅𝑢𝑎𝑙𝑙𝑃𝑜𝑠𝑖𝑡𝑖𝑜𝑛\mathit{pNode}.\mathit{RuallPosition}. Otherwise pOppOp may miss such an update operation whose key should be used as a candidate return value. To see why, we consider the following execution where pOppOp does not use atomic copy, which is depicted in Figure 7. The RU-ALL contains an update node, 𝑢𝑁𝑜𝑑𝑒20\mathit{uNode}_{20}, with key 2020, and the two sentinel nodes with keys \infty and -\infty. A Predecessor(40)(40) operation, pOppOp, reads a pointer to 𝑢𝑁𝑜𝑑𝑒20\mathit{uNode}_{20} during the first step of its traversal of the RU-ALL, but it does not yet write it into its predecessor node, 𝑝𝑁𝑜𝑑𝑒\mathit{pNode}. An SS-modifying Delete(25)(25) operation, dOp25dOp_{25}, is linearized, and then an SS-modifying Delete(29)(29) operation, dOp29dOp_{29}, is linearized. These DEL nodes of these Delete operations are not seen by pOppOp because they are added to the RU-ALL before pOppOp’s current location in the RU-ALL. Then dOp29dOp_{29} attempts to notify pOppOp. It reads that 𝑝𝑁𝑜𝑑𝑒.𝑅𝑢𝑎𝑙𝑙𝑃𝑜𝑠𝑖𝑡𝑖𝑜𝑛\mathit{pNode}.\mathit{RuallPosition} points to the sentinel node with key \infty, so dOp29dOp_{29} writes \infty to the notify threshold of its notification. Hence, the notification is rejected by pOppOp. Now pOppOp writes the pointer to 𝑢𝑁𝑜𝑑𝑒20\mathit{uNode}_{20} to 𝑝𝑁𝑜𝑑𝑒.𝑅𝑢𝑎𝑙𝑙𝑃𝑜𝑠𝑖𝑡𝑖𝑜𝑛\mathit{pNode}.\mathit{RuallPosition}. So pOppOp begins accepting the notifications of Delete operations with key greater than 20. When dOp25dOp_{25} attempts to notify pOppOp, it reads that 𝑝𝑁𝑜𝑑𝑒.𝑅𝑢𝑎𝑙𝑙𝑃𝑜𝑠𝑖𝑡𝑖𝑜𝑛\mathit{pNode}.\mathit{RuallPosition} points to 𝑢𝑁𝑜𝑑𝑒25\mathit{uNode}_{25}, so it writes 2525 to the notify threshold of its notification. Hence, the notification is accepted by pOppOp, and 25 is a candidate return value of pOppOp. In all configurations in which 25 is in SS, the key 2929 is also in SS. So pOppOp should not return 25. By using atomic copy, either both 25 and 29 will be added as candidate return values, or neither will be.

Refer to caption
Figure 7: Example execution of a Predecessor(40)(40), Delete(25)(25), and Delete(29)(29). The vertical dashed lines indicate when a Delete operation notifies the Predecessor(40)(40) operation.

Traversing the relaxed binary trie: Following pOppOp’s traversal of the RU-ALL, pOppOp traverses the relaxed binary trie using RelaxedPredecessor(y)(y) (on line 202). This has been described in Section 4. If it returns a value other than \bot, the value is a candidate return value of pOppOp.


Traversing the U-ALL: The traversal of the U-ALL is done in TraverseUall(y)(y) (on line 203). Recall that it returns two sets of update nodes I𝑢𝑎𝑙𝑙I_{\mathit{uall}} and D𝑢𝑎𝑙𝑙D_{\mathit{uall}}. The keys of INS nodes in I𝑢𝑎𝑙𝑙I_{\mathit{uall}} are candidate return values. The keys of DEL nodes in D𝑢𝑎𝑙𝑙D_{\mathit{uall}} not seen during pOppOp’s traversal of RU-ALL are candidate return values. This is because the Delete operations that created these DEL nodes are linearized sometime during pOppOp.


Collecting notifications: The collection of pOppOp’s notifications is done on line 205 to line 224. Consider a notify node, 𝑛𝑁𝑜𝑑𝑒\mathit{nNode}, created by an update operation, uOpuOp, with key less than yy that pOppOp encounters in its 𝑛𝑜𝑡𝑖𝑓𝑦𝐿𝑖𝑠𝑡\mathit{notifyList} at the beginning of the for-loop on line 205. If 𝑛𝑁𝑜𝑑𝑒\mathit{nNode} was created by an Insert operation, then 𝑛𝑁𝑜𝑑𝑒\mathit{nNode} is accepted if 𝑛𝑁𝑜𝑑𝑒.𝑛𝑜𝑡𝑖𝑓𝑦𝑇ℎ𝑟𝑒𝑠ℎ𝑜𝑙𝑑\mathit{nNode}.\mathit{notifyThreshold} is less than or equal to 𝑛𝑁𝑜𝑑𝑒.𝑘𝑒𝑦\mathit{nNode}.\mathit{key}. In this case, the update node created by the Insert operation is put into pOppOp’s local set I𝑛𝑜𝑡𝑖𝑓𝑦I_{\mathit{notify}}. If 𝑛𝑁𝑜𝑑𝑒\mathit{nNode} was created by a Delete operation, then 𝑛𝑁𝑜𝑑𝑒\mathit{nNode} is accepted if 𝑛𝑁𝑜𝑑𝑒.𝑛𝑜𝑡𝑖𝑓𝑦𝑇ℎ𝑟𝑒𝑠ℎ𝑜𝑙𝑑\mathit{nNode}.\mathit{notifyThreshold} is less than 𝑛𝑁𝑜𝑑𝑒.𝑘𝑒𝑦\mathit{nNode}.\mathit{key}. In this case, the update node created by the Delete operation is put into pOppOp’s local set D𝑛𝑜𝑡𝑖𝑓𝑦D_{\mathit{notify}}. Note that, if 𝑛𝑁𝑜𝑑𝑒\mathit{nNode} is not accepted, it may still be used in the sixth part of the algorithm.

Recall that 𝑛𝑁𝑜𝑑𝑒.𝑢𝑝𝑑𝑎𝑡𝑒𝑁𝑜𝑑𝑒𝑀𝑎𝑥\mathit{nNode}.\mathit{updateNodeMax} is the INS node with largest key less than yy that uOpuOp identified during its traversal of the U-ALL. This is performed before uOpuOp notifies any Predecessor operations. Operation pOppOp also determines if the key of this INS node should be used as a candidate return value: If 𝑛𝑁𝑜𝑑𝑒.𝑛𝑜𝑡𝑖𝑓𝑦𝑇ℎ𝑟𝑒𝑠ℎ𝑜𝑙𝑑=\mathit{nNode}.\mathit{notifyThreshold}=-\infty (indicating that pOppOp had completed its traversal of the RU-ALL when pOppOp was notified) and 𝑛𝑁𝑜𝑑𝑒.𝑢𝑝𝑑𝑎𝑡𝑒𝑁𝑜𝑑𝑒\mathit{nNode}.\mathit{updateNode} is not an update node in I𝑟𝑢𝑎𝑙𝑙I_{\mathit{ruall}} or D𝑟𝑢𝑎𝑙𝑙D_{\mathit{ruall}} (checked on line 212), then 𝑛𝑁𝑜𝑑𝑒.𝑢𝑝𝑑𝑎𝑡𝑒𝑁𝑜𝑑𝑒𝑀𝑎𝑥\mathit{nNode}.\mathit{updateNodeMax} is also added to I𝑛𝑜𝑡𝑖𝑓𝑦I_{\mathit{notify}}. We need to ensure that pOppOp does not miss relevant Insert operations linearized after pOppOp completed its traversal of the U-ALL and before uOpuOp is linearized. These Insert operations might not notify pOppOp, and their announcements are not seen by pOppOp when it traverses the U-ALL. We guarantee that 𝑛𝑁𝑜𝑑𝑒.𝑢𝑝𝑑𝑎𝑡𝑒𝑁𝑜𝑑𝑒𝑀𝑎𝑥\mathit{nNode}.\mathit{updateNodeMax} is the INS node with largest key less than yy that falls into this category. For example, consider the execution shown in Figure 8. Let ww, xx, and yy be three keys where w<x<yw<x<y. An Insert(x)(x) operation, iOpxiOp_{x} is linearized before an Insert(w)(w) operation, iOpwiOp_{w}, and both are linearized after a Predecessor(y)(y) operation, pOppOp, has completed its traversal of the U-ALL. Suppose iOpwiOp_{w} notifies pOppOp, but iOpxiOp_{x} does not. Then ww is a candidate return value of pOppOp. Note that pOppOp does not see the announcement of iOpxiOp_{x} when it traverses the U-ALL. In this execution, xSx\in S whenever wSw\in S. Since pOppOp returns its largest candidate return value and ww is a candidate return value, pOppOp must determine a candidate return value at least xx. The INS node of iOpxiOp_{x} is in the U-ALL throughout iOpwiOp_{w}’s traversal of the U-ALL and, hence, is seen by iOpwiOp_{w}. So, when iOpwiOp_{w} notifies pOppOp, iOpwiOp_{w} will set 𝑢𝑝𝑑𝑎𝑡𝑒𝑁𝑜𝑑𝑒𝑀𝑎𝑥\mathit{updateNodeMax} to point to this INS node. Hence, xx is a candidate return value of pOppOp.

Refer to caption
Figure 8: Example execution of a Predecessor(y)(y), Insert(w)(w), and Insert(x)(x), where w<x<yw<x<y.

When the traversal of the relaxed binary trie returns \bot: Let kk be the largest key less than yy that is completely present throughout pOppOp’s traversal of the relaxed binary trie, or 1-1 if no such key exists. If pOppOp’s traversal returns \bot, then by the specification of the relaxed binary trie, there is an SS-modifying update operation uOpuOp with key xx, where k<x<yk<x<y, whose update to the relaxed binary trie is concurrent with pOppOp’s traversal of the relaxed binary trie. The update node created by uOpuOp is encountered by pOppOp either in the U-ALL or in its notify list. This is because either pOppOp will traverse the U-ALL before uOpuOp can remove its update node from the U-ALL, or uOpuOp will notify pOppOp before pOppOp removes its predecessor node from the P-ALL. Unless uOpuOp is a Delete(x)(x) operation whose DEL node is in D𝑟𝑢𝑎𝑙𝑙D_{\mathit{ruall}}, xx is a candidate return value. This gives the following observation: If p1<kp_{1}<k (on line 215), then there is a DEL node in D𝑟𝑢𝑎𝑙𝑙D_{\mathit{ruall}} with key xx such that k<x<yk<x<y.

When the traversal of the relaxed binary trie returns \bot and D𝑟𝑢𝑎𝑙𝑙D_{\mathit{ruall}} is non-empty, pOppOp takes additional steps to guarantee it has a candidate return value at least kk (by executing lines 217 to 237). This is done by using the keys and results of embedded predecessor operations of update operations linearized before the start of pOppOp’s traversal of the relaxed binary trie, and possibly before the start of pOppOp. First, pOppOp determines the predecessor nodes created by the first embedded predecessor operations of DEL nodes in D𝑟𝑢𝑎𝑙𝑙D_{\mathit{ruall}}. If pOppOp encounters one of these predecessor nodes when it traversed the P-ALL, pOppOp sets 𝑝𝑁𝑜𝑑𝑒\mathit{pNode}^{\prime} to be the one it encountered the latest in the P-ALL (on line 221). Note that 𝑝𝑁𝑜𝑑𝑒\mathit{pNode}^{\prime} was announced the earliest among these predecessor nodes and also announced earlier than 𝑝𝑁𝑜𝑑𝑒\mathit{pNode}. Then pOppOp traverses 𝑝𝑁𝑜𝑑𝑒.𝑛𝑜𝑡𝑖𝑓𝑦𝐿𝑖𝑠𝑡\mathit{pNode}^{\prime}.\mathit{notifyList} to determine the update nodes of update operations with key less than yy that notified 𝑝𝑁𝑜𝑑𝑒\mathit{pNode}^{\prime} (on lines 222 to 223). These update nodes are stored in a local sequence, L1L_{1}, and appear in the order in which their notifications were added to 𝑝𝑁𝑜𝑑𝑒.𝑛𝑜𝑡𝑖𝑓𝑦𝐿𝑖𝑠𝑡\mathit{pNode}^{\prime}.\mathit{notifyList}.

Next, pOppOp traverses 𝑝𝑁𝑜𝑑𝑒.𝑛𝑜𝑡𝑖𝑓𝑦𝐿𝑖𝑠𝑡\mathit{pNode}.\mathit{notifyList} to determine the update nodes of update operations with key less than yy that notified 𝑝𝑁𝑜𝑑𝑒\mathit{pNode}. Those belonging to notifications whose 𝑛𝑜𝑡𝑖𝑓𝑦𝑇ℎ𝑟𝑒𝑠ℎ𝑜𝑙𝑑\mathit{notifyThreshold} are greater than or equal to the key of the notification are stored in a local sequence, L2L_{2} (on line 227), while the others are stored in a local set, LL^{\prime} (on line 229). The update nodes in L2L_{2} appear in the order in which their notifications were added to 𝑝𝑁𝑜𝑑𝑒.𝑛𝑜𝑡𝑖𝑓𝑦𝐿𝑖𝑠𝑡\mathit{pNode}.\mathit{notifyList}, and were added to 𝑝𝑁𝑜𝑑𝑒.𝑛𝑜𝑡𝑖𝑓𝑦𝐿𝑖𝑠𝑡\mathit{pNode}.\mathit{notifyList} before pOppOp completed its traversal of the RU-ALL. It includes the update nodes of update operations whose notifications were rejected by pOppOp, and may include some INS nodes of Insert operations whose notifications were accepted by pOppOp. The local sequence LL is L1L_{1} followed by L2L_{2}, excluding the update nodes in LL^{\prime} (computed on line 230). The update nodes in LL^{\prime} are excluded from LL so that LL only contains update nodes belonging to update operations linearized before the start of pOppOp, or update operations that notified pOppOp before pOppOp started its traversal of the relaxed binary trie.

A candidate return value is then computed from LL and D𝑟𝑢𝑎𝑙𝑙D_{\mathit{ruall}} (on lines 231 to line 237). If 𝑝𝑁𝑜𝑑𝑒\mathit{pNode}^{\prime} was set, let CC be the configuration immediately after 𝑝𝑁𝑜𝑑𝑒\mathit{pNode}^{\prime} was announced; otherwise let CC be the configuration immediately after 𝑝𝑁𝑜𝑑𝑒\mathit{pNode} was announced. Each key wRw\in R is in SS sometime between CC and the start of pOppOp’s traversal of the relaxed binary trie. However, it may be deleted from SS before pOppOp begins its traversal of the relaxed binary trie. For example, if the last update node in LL with key ww is a DEL node, then ww was deleted from SS before pOppOp begins its traversal of the relaxed binary trie. Such keys are removed from RR (on line 235). It is replaced with the value of the second embedded predecessor stored in this DEL node. For the same reason, the keys of DEL nodes in D𝑟𝑢𝑎𝑙𝑙D_{\mathit{ruall}} are removed from RR (on line 236). The largest remaining key in RR, which we can guarantee is non-empty, is a candidate return value of pOppOp.

We next explain in more detail why pOppOp determines a candidate return value at least kk. Suppose that immediately after pOppOp has completed collecting notifications, it has not determined a candidate return value at least kk. In other words, p1<kp_{1}<k (on line 215). Consider the Insert(k)(k) operation, iOpiOp, that last added kk to SS prior to the start of pOppOp’s traversal of the relaxed binary trie. The INS node of iOpiOp was not seen when pOppOp traversed the U-ALL and pOppOp did not accept a notification from iOpiOp, so iOpiOp must have completed sometime before the start of pOppOp’s traversal of the relaxed binary trie. We show that, on line 237, RR contains a value at which is least kk and, hence, pOppOp returns a value which is at least kk (on line 238).

Suppose iOpiOp is linearized after CC. Since 𝑖𝑁𝑜𝑑𝑒I𝑢𝑎𝑙𝑙I𝑛𝑜𝑡𝑖𝑓𝑦\mathit{iNode}\notin I_{\mathit{uall}}\cup I_{\mathit{notify}}, if iOpiOp notifies 𝑝𝑁𝑜𝑑𝑒\mathit{pNode}, the notification must be rejected, and hence iOpiOp notified pOppOp when 𝑝𝑁𝑜𝑑𝑒.𝑅𝑢𝑎𝑙𝑙𝑃𝑜𝑠𝑖𝑡𝑖𝑜𝑛\mathit{pNode}.\mathit{RuallPosition} points to an update node with key greater than kk. It follows that 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} is added to L2L_{2} on line 227. Otherwise iOpiOp does not notify 𝑝𝑁𝑜𝑑𝑒\mathit{pNode}, so it notifies 𝑝𝑁𝑜𝑑𝑒\mathit{pNode}^{\prime}. Then 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} is added to L1L_{1}. In any case, kk is a key in RR. So kk is the key of an INS node in LL, and hence is added to RR on line 233. By assumption, there are no Delete(k)(k) operations linearized after iOpiOp and before the end of pOppOp’s traversal of the relaxed binary trie. Since LL only contains the update nodes of update operations linearized before the start of pOppOp’s traversal of the relaxed binary trie, the last update node with key kk in LL is iOpiOp’s INS node. So kk is not removed from RR on line 235. Furthermore, D𝑟𝑢𝑎𝑙𝑙D_{\mathit{ruall}} only contains the DEL nodes of Delete operations linearized before the start of pOppOp’s traversal of the relaxed binary trie. For contradiction, suppose there is a DEL node in D𝑟𝑢𝑎𝑙𝑙D_{\mathit{ruall}} with key kk. Then pOppOp encountered this DEL node in RU-ALL and simultaneously set 𝑝𝑁𝑜𝑑𝑒.𝑅𝑢𝑎𝑙𝑙𝑃𝑜𝑠𝑖𝑡𝑖𝑜𝑛\mathit{pNode}.\mathit{RuallPosition} to point to an update node with key kk. From this point on, pOppOp accepts all notifications from Insert(k)(k) operations. When pOppOp put this DEL node into D𝑟𝑢𝑎𝑙𝑙D_{\mathit{ruall}}, it was the latest update operation with key kk. Therefore, iOpiOp was linearized after this point. Hence, either iOpiOp notified pOppOp or pOppOp encountered iOpiOp’s INS node when it traversed the U-ALL. This contradicts the fact that 𝑖𝑁𝑜𝑑𝑒I𝑢𝑎𝑙𝑙I𝑛𝑜𝑡𝑖𝑓𝑦\mathit{iNode}\notin I_{\mathit{uall}}\cup I_{\mathit{notify}}. Thus, kk is not removed from RR on line 236, and RR contains a value at which is least kk.

Now suppose iOpiOp was linearized before CC. So kSk\in S in all configurations between CC and the end of pOppOp’s traversal of the relaxed binary trie. Note that CC occurs before the start of the first embedded predecessor operation of any DEL node in D𝑟𝑢𝑎𝑙𝑙D_{\mathit{ruall}}. Recall the observation that when p1<kp_{1}<k (on line 215), there is a DEL node 𝑑𝑁𝑜𝑑𝑒D𝑟𝑢𝑎𝑙𝑙\mathit{dNode}\in D_{\mathit{ruall}} with key xx such that k<x<yk<x<y. The first embedded predecessor of the Delete(x)(x) operation, dOpxdOp_{x}, that created 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} begins after CC. From the code, this embedded predecessor operation completes before 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} is added to the RU-ALL. Since pOppOp added 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} to D𝑟𝑢𝑎𝑙𝑙D_{\mathit{ruall}} while it traversed the RU-ALL, 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} was added to the RU-ALL before pOppOp began its traversal of the relaxed binary trie. The first embedded predecessor of dOpxdOp_{x} returns a value kk^{\prime} such that kk<xk\leq k^{\prime}<x, because kSk\in S throughout its execution interval. This value will be added to RR on line 231. So RR contains at least one value at least kk at this point.

We will prove (in Lemma 5.27) the following claim: If a key that is at least kk is removed from RR on line 235 during some iteration of pOppOp’s for-loop on line 232, a smaller key that is at least kk will be added to RR in the same or later configuration of the for-loop. Since RR contains at least one value at least kk before the for-loop on line 232, this claim implies that RR contains a value at least kk after the for-loop. Let k′′k^{\prime\prime} be the smallest value k′′kk^{\prime\prime}\geq k that is in RR immediately before line 236 (i.e. immediately after pOppOp completes its local traversal of LL during the for-loop on line 232). Suppose, for contradiction, that k′′k^{\prime\prime} is removed from RR on line 236. Then there exists a DEL node, 𝑑𝑁𝑜𝑑𝑒D𝑟𝑢𝑎𝑙𝑙\mathit{dNode}^{\prime}\in D_{\mathit{ruall}} such that 𝑑𝑁𝑜𝑑𝑒.𝑘𝑒𝑦=k′′\mathit{dNode}^{\prime}.\mathit{key}=k^{\prime\prime}. By definition of CC, its first embedded predecessor occurs after CC. So the first embedded predecessor of 𝑑𝑁𝑜𝑑𝑒\mathit{dNode}^{\prime} returns a key k′′′k^{\prime\prime\prime} where kk′′′<k′′k\leq k^{\prime\prime\prime}<k^{\prime\prime}. The claim implies that, immediately before line 236, RR contains a key at least k′′′k^{\prime\prime\prime}. This contradicts the definition of k′′k^{\prime\prime}. Therefore, p0p_{0} is set to k′′kk^{\prime\prime}\geq k on line 237.

5.3 Linearizability

This section shows that our implementation of the lock-free binary trie is linearizable. We first prove basic properties about the latest lists in Section 5.3.1. We then show that our implementation is linearizable with respect to Search, Insert, and Delete operations in Section 5.3.2.

Recall that in a configuration CC, the predecessor of yy is the key ww such that wSw\in S and there is no key xSx\in S such that w<x<yw<x<y, otherwise it is 1-1 if there is no key in SS smaller than yy. We show that if a Predecessor(y)(y) operation returns ww, then there is a configuration CC during its execution interval in which ww is the predecessor of yy. To show that our implementation is linearizble, each completed Predecessor(y)(y) operation can be linearized at any such configuration.

Recall that our implementation of Predecessor, described in Section 5.2.4, determines a number of candidate return values, and returns the largest of them. In Section 5.3.3, we first define three properties, denoted Properties 1, 2, and 3, that these candidate return values will satisfy. Additionally, we prove that any implementation of Predecessor whose candidate return values satisfies these properties, together with our implementations of Search, Insert, and Delete, results in a linearizable implementation of a lock-free binary trie. In Section 5.3.4, we show that, the candidate return values determined by our implementation satisfy Property 1. We prove that Properties 2 and 3 are satisfied in Sections 5.3.5 and 5.3.6.

5.3.1 Properties of the Latest Lists

In this section, we prove basic facts about the latest[x]\textit{latest}[x] lists, for each xUx\in U. They are used to show the linearizability of TrieInsert, TrieDelete, and TrieSearch.

Lemma 5.1.

Let 𝑢𝑁𝑜𝑑𝑒\mathit{uNode} be an updated node in latest[x]\textit{latest}[x]. If 𝑢𝑁𝑜𝑑𝑒.𝑠𝑡𝑎𝑡𝑢𝑠=Inactive\mathit{uNode}.\mathit{status}=\textsc{Inactive}, then 𝑢𝑁𝑜𝑑𝑒\mathit{uNode} is not the last node in latest[x]\textit{latest}[x] (i.e. 𝑢𝑁𝑜𝑑𝑒.𝑙𝑎𝑡𝑒𝑠𝑡𝑁𝑒𝑥𝑡\mathit{uNode}.\mathit{latestNext}\neq\bot).

Proof.

When 𝑢𝑁𝑜𝑑𝑒\mathit{uNode} is initialized, its status is Inactive and its 𝑙𝑎𝑡𝑒𝑠𝑡𝑁𝑒𝑥𝑡\mathit{latestNext} field is also initialized to an update node. By inspection of the algorithms for Insert and Delete, the field 𝑙𝑎𝑡𝑒𝑠𝑡𝑁𝑒𝑥𝑡\mathit{latestNext} is only set to \bot after some process performs a CAS that changes the status of 𝑢𝑁𝑜𝑑𝑒\mathit{uNode} from Inactive to Active. ∎

Lemma 5.2.

For each xUx\in U, the length of latest[x]\textit{latest}[x] is either 1 or 2.

Proof.

Initially, latest[x].ℎ𝑒𝑎𝑑\textit{latest}[x].\mathit{head} points to a dummy DEL node whose 𝑙𝑎𝑡𝑒𝑠𝑡𝑁𝑒𝑥𝑡\mathit{latestNext} is set to \bot. Let 𝑢𝑁𝑜𝑑𝑒\mathit{uNode} be the update node pointed to by latest[x].ℎ𝑒𝑎𝑑\textit{latest}[x].\mathit{head}. New update nodes are only added to latest[x]\textit{latest}[x] by updating latest[x].ℎ𝑒𝑎𝑑\textit{latest}[x].\mathit{head} to point to a different update node 𝑢𝑁𝑜𝑑𝑒\mathit{uNode}^{\prime} using CAS(latest[x].ℎ𝑒𝑎𝑑,𝑢𝑁𝑜𝑑𝑒,𝑢𝑁𝑜𝑑𝑒)(\textit{latest}[x].\mathit{head},\mathit{uNode},\mathit{uNode}^{\prime}) (on line  155 or 177), where 𝑢𝑁𝑜𝑑𝑒.𝑙𝑎𝑡𝑒𝑠𝑡𝑁𝑒𝑥𝑡=𝑢𝑁𝑜𝑑𝑒\mathit{uNode}^{\prime}.\mathit{latestNext}=\mathit{uNode}. Immediately before this CAS, 𝑢𝑁𝑜𝑑𝑒.𝑙𝑎𝑡𝑒𝑠𝑡𝑁𝑒𝑥𝑡\mathit{uNode}.\mathit{latestNext} is set to \bot. ∎

Lemma 5.3.

Let τ\tau be a completed instance of FindLatest(x)(x) that returns an update node \ell. Then there is a configuration during τ\tau in which \ell is the first activated node in latest[x]\textit{latest}[x].

Proof.

Suppose that the operation opop reads that .𝑠𝑡𝑎𝑡𝑢𝑠Inactive\ell.\mathit{status}\neq\textsc{Inactive} on line 105. Then \ell is the first activated node in latest[x]\textit{latest}[x] some time between the read of the pointer to \ell and the read that .𝑠𝑡𝑎𝑡𝑢𝑠Inactive\ell.\mathit{status}\neq\textsc{Inactive}. Since opop returns \ell, the lemma holds.

Now suppose opop reads that .𝑠𝑡𝑎𝑡𝑢𝑠=Inactive\ell.\mathit{status}=\textsc{Inactive}. Suppose opop reads that .𝑙𝑎𝑡𝑒𝑠𝑡𝑁𝑒𝑥𝑡=\ell.\mathit{latestNext}=\bot on line 107. Lemma 5.1 implies that the status of \ell was changed to Active sometime between opop’s read that .𝑠𝑡𝑎𝑡𝑢𝑠=Inactive\ell.\mathit{status}=\textsc{Inactive} and .𝑙𝑎𝑡𝑒𝑠𝑡𝑁𝑒𝑥𝑡=\ell.\mathit{latestNext}=\bot. Since opop returns \ell, the lemma holds.

So opop reads that .𝑙𝑎𝑡𝑒𝑠𝑡𝑁𝑒𝑥𝑡=m\ell.\mathit{latestNext}=m\neq\bot on line 107. Then mm is an activated update node. Once .𝑙𝑎𝑡𝑒𝑠𝑡𝑁𝑒𝑥𝑡\ell.\mathit{latestNext} is initialized to point to mm, it does not change to any other value except for \bot. So mm is the first activated update node in latest[x]\textit{latest}[x] immediately after opop reads .𝑠𝑡𝑎𝑡𝑢𝑠=Inactive\ell.\mathit{status}=\textsc{Inactive}. Since opop returns mm, the lemma holds. ∎

Lemma 5.4.

Let τ\tau be a completed instance of FirstActivated(v)(v), where vv is a pointer to an activated update node. If τ\tau returns True, there is a configuration during τ\tau in which vv is the first activated node in latest[v.𝑘𝑒𝑦]\textit{latest}[v.\mathit{key}].

Proof.

Let \ell be the pointer to the update node pointed to by latest[v.𝑘𝑒𝑦].ℎ𝑒𝑎𝑑\textit{latest}[v.\mathit{key}].\mathit{head} read on line 114. Suppose τ\tau returns True because =v\ell=v. Then vv is the first activated node in latest[v.𝑘𝑒𝑦]\textit{latest}[v.\mathit{key}] immediately after the read of \ell.

So suppose τ\tau returns True because .status=Inactive\ell.status=\textsc{Inactive} and v=.𝑙𝑎𝑡𝑒𝑠𝑡𝑁𝑒𝑥𝑡v=\ell.\mathit{latestNext}. Then vv is the first activated update node in latest[v.𝑘𝑒𝑦]\textit{latest}[v.\mathit{key}] in the configuration immediately after the read of .status=Inactive\ell.status=\textsc{Inactive}. ∎

Lemma 5.5.

Let τ\tau be a completed instance of FirstActivated(v)(v), where vv is a pointer to an activated update node. If τ\tau returns False, there is a configuration during τ\tau in which vv is not the first activated node in latest[v.𝑘𝑒𝑦]\textit{latest}[v.\mathit{key}].

Proof.

Let \ell be the pointer to the update node pointed to by latest[v.𝑘𝑒𝑦].ℎ𝑒𝑎𝑑\textit{latest}[v.\mathit{key}].\mathit{head} read on line 114. Suppose τ\tau returns False because v\ell\neq v and .𝑠𝑡𝑎𝑡𝑢𝑠=Active\ell.\mathit{status}=\textsc{Active}. Then in the configuration immediately after the read of .𝑠𝑡𝑎𝑡𝑢𝑠\ell.\mathit{status}, vv is not the first activated update node in latest[v.𝑘𝑒𝑦]\textit{latest}[v.\mathit{key}].

So suppose τ\tau returns False because v\ell\neq v and v.𝑙𝑎𝑡𝑒𝑠𝑡𝑁𝑒𝑥𝑡v\neq\ell.\mathit{latestNext}. By Lemma 5.2, latest[v.𝑘𝑒𝑦]\textit{latest}[v.\mathit{key}] has length at most 2, and update nodes removed from latest[v.𝑘𝑒𝑦]\textit{latest}[v.\mathit{key}] are never added back. Then in the configuration immediately after the read of .𝑙𝑎𝑡𝑒𝑠𝑡𝑁𝑒𝑥𝑡\ell.\mathit{latestNext}, vv is not in latest[v.𝑘𝑒𝑦]\textit{latest}[v.\mathit{key}], and hence not the first activated update node in latest[v.𝑘𝑒𝑦]\textit{latest}[v.\mathit{key}]. ∎

5.3.2 Linearizability of Insert, Delete, and Search

A Search(x)(x) operation that returns True is linearized in any configuration during its execution interval in which xSx\in S. The next lemma proves such a configuration exists.

Lemma 5.6.

Suppose opop is a Search(x)(x) operation that returns True. Then there exists a configuration during opop in which xSx\in S.

Proof.

Search(x)(x) begins by calling FindLatest(x)(x) on line 109, which returns an update node 𝑢𝑁𝑜𝑑𝑒\mathit{uNode}. By Lemma 5.3, there is a configuration CC during this instance of FindLatest(x)(x) in which 𝑢𝑁𝑜𝑑𝑒\mathit{uNode} is the first activated node in latest[x]\textit{latest}[x]. Since opop returned True, it read that 𝑢𝑁𝑜𝑑𝑒.𝑡𝑦𝑝𝑒=INS\mathit{uNode}.\mathit{type}=\textsc{INS}. By definition, xSx\in S in CC. ∎

A Search(x)(x) operation that returns False is linearized in any configuration during its execution interval in which xSx\notin S.

Lemma 5.7.

Suppose opop is a Search(x)(x) operation that returns False. Then there exists a configuration during opop in which xSx\notin S.

Proof.

Search(x)(x) begins by calling FindLatest(x)(x) on line 109, which returns an update node 𝑢𝑁𝑜𝑑𝑒\mathit{uNode}. By Lemma 5.3, there is a configuration CC during this instance of FindLatest(x)(x) in which 𝑢𝑁𝑜𝑑𝑒\mathit{uNode} is the first activated node in latest[x]\textit{latest}[x]. Since opop returned False, it read that 𝑢𝑁𝑜𝑑𝑒.𝑡𝑦𝑝𝑒=DEL\mathit{uNode}.\mathit{type}=\textsc{DEL}. By definition, xSx\notin S in CC. ∎

Recall that the linearization points of an SS-modifying Insert or Delete operation is immediately after when status of the update node it created is changed from Inactive to Active.

An Insert(x)(x) operation that is not SS-modifying does not update latest[x]\textit{latest}[x] to point to its own update node. This happens when it reads that the first activated update node in latest[x]\textit{latest}[x] is a DEL node, or when it performs an unsuccessful CAS. In the following two lemmas, we prove that for each of these two cases, there is a configuration during the Insert(x)(x) in which xSx\in S, and hence does not need to add xx to SS. Likewise, a Delete(x)(x) operation may return early before activating its update node because there is a configuration in which xSx\notin S, and hence does not need to remove xx from SS.

Lemma 5.8.

Suppose uOpuOp is a Insert(x)(x) operation (or a Delete(x)(x) operation) that returns on line 150 (or on line 168). Then there is a configuration in which uOpuOp in which xSx\in S (or xSx\notin S).

Proof.

Insert(x)(x) begins by calling FindLatest(x)(x) on line 28 (or line 167 of Delete), which returns an update node 𝑢𝑁𝑜𝑑𝑒\mathit{uNode}. By Lemma 5.3, there is a configuration CC during this instance of FindLatest(x)(x) in which 𝑢𝑁𝑜𝑑𝑒\mathit{uNode} is the first activated node in latest[x]\textit{latest}[x]. Since uOpuOp returned on line 150 (or on line 150 for Delete), it saw that 𝑢𝑁𝑜𝑑𝑒.𝑡𝑦𝑝𝑒=INS\mathit{uNode}.\mathit{type}=\text{INS} (or 𝑢𝑁𝑜𝑑𝑒.𝑡𝑦𝑝𝑒=DEL\mathit{uNode}.\mathit{type}=\text{DEL} for Delete). By definition, xSx\in S (or xSx\notin S) in CC. ∎

Lemma 5.9.

Suppose uOpuOp is a Insert(x)(x) operation (or a Delete(x)(x) operation) that returns on line 157 (or on line 180). Then there is a configuration in which uOpuOp in which xSx\in S (or xSx\notin S).

Proof.

We prove the case when uOpuOp is an Insert(x)(x) operation. The Delete(x)(x) case follows similarly.

Since uOpuOp performs an unsuccessful CAS on line 155, some other Insert(x)(x) operation added its INS node 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} to the head of latest[x]\textit{latest}[x] since uOpuOp last read that latest[x].head\textit{latest}[x].head pointed to a 𝑑𝑁𝑜𝑑𝑒\mathit{dNode}. So uOpuOp calls HelpActivate on the update node pointed to by latest[x].head\textit{latest}[x].head. If this update node is still 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} and its inactive, then 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} will be activated by uOpuOp on line 119. If the update node pointed to by latest[x].head\textit{latest}[x].head is not 𝑖𝑁𝑜𝑑𝑒\mathit{iNode}, then by 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} was activated by some other Insert(x)(x). In the configuration immediately after 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} is activated, xSx\in S. ∎

5.3.3 Properties of Candidate Return Values

In this section, we state properties of the candidate return values of a Predecessor(y)(y) operation, and prove that the value returned by this operation is correct, assuming these properties hold.

Each candidate return value of a Predecessor operation is a value in U{1}U\cup\{-1\}. Recall that Predecessor operations announce themselves at the start of their operation, and remove the announcement at the end of their operation. Before this announcement is removed, they notify Predecessor operations that have announced themselves. Intuitively, the candidate return values of a Predecessor operation are a subset of the values read from its traversal of the relaxed binary trie, the announcements, or its notifications.

In the following properties, we let pOppOp be a completed Predecessor(y)(y) operation. Let CTC_{T} be the configuration immediately before pOppOp begins its traversal of the relaxed binary trie.

Property 1.

All candidate return values of pOppOp are less than yy, and pOppOp returns its largest candidate return value.

For each of pOppOp’s candidate return values w1w\neq-1, there is a configuration CC during pOppOp in which wSw\in S. Furthermore, it states that the keys of certain update operations that are concurrent with pOppOp are also candidate return values of pOppOp. This property (together with the next property) is used to argue that all keys between ww and yy that are also in SS in CC are candidate return values of pOppOp.

Property 2.

Suppose w1w\neq-1 is a candidate return value of pOppOp. Then there is a configuration CC during pOppOp such that

  1. (a)

    wSw\in S,

  2. (b)

    if CC occurs before CTC_{T} and there exists an SS-modifying Delete(x)(x) operation linearized between CC and CTC_{T} with w<x<yw<x<y, then pOppOp has a candidate return value which is at least xx, and

  3. (c)

    if CC occurs after CTC_{T} and there exists an SS-modifying Insert(x)(x) operation linearized between CTC_{T} and CC with w<x<yw<x<y, then pOppOp has a candidate return value which is at least xx.

The next property states that pOppOp should learn about keys xx in SS that have been added to SS before the start of pOppOp’s traversal of the binary trie. If pOppOp is does not learn about xx, perhaps because there are no concurrent update operations with key xx, then pOppOp’s traversal of the relaxed binary trie returns a key at which is least xx.

Property 3.

Suppose an SS-modifying Insert(x)(x) operation iOpiOp is linearized before CTC_{T}, x<yx<y, and there are no SS-modifying Delete(x)(x) operations linearized after iOpiOp and before CTC_{T}. Then pOppOp has a candidate return value which is at least xx.

In a configuration CC, the predecessor of yy is 1-1 if there is no key in SS smaller than yy, otherwise it is the key ww such that wSw\in S and there is no key xSx\in S where w<x<yw<x<y. The next lemma states that value returned by pOppOp is a predecessor of yy in some configuration during pOppOp. This is the linearization point of pOppOp. So any predecessor algorithm that satisfies Property 1, Property 2, and Property 3 results in a linearizable implementation.

Theorem 5.10.

If pOppOp returns wU{1}w\in U\cup\{-1\}, then there exists a configuration during pOppOp in which ww is the predecessor of yy.

Proof.

Suppose pOppOp returns 1-1. We show that there is no key xSx\in S in CTC_{T}, where x<yx<y. Suppose, for contradiction, that there is a key xSx\in S in CTC_{T}, where x<yx<y. Let iOpiOp be the Insert(x)(x) operation that last added xx to SS before CTC_{T}. So there are no Delete(x)(x) operations linearized after iOpiOp but before CTC_{T}. By Property 3, there is a key xx^{\prime} where xx<yx\leq x^{\prime}<y that is a candidate return value of pOppOp. This contradicts Property 1.

So suppose pOppOp returns wUw\in U. By Property 1, ww is a candidate return value of pOppOp. Let CC be the configuration during pOppOp defined in Property 2. By Property 2(a), wSw\in S in CC. To show that ww is the predecessor of yy in CC, it remains to show that there is no key xSx\in S in CC, where w<x<yw<x<y.

Suppose, for contradiction, that there is a key xSx\in S in CC, where w<x<yw<x<y. Let iOpiOp be the Insert(x)(x) operation that last added xx to SS before CC. So suppose iOpiOp is linearized after CTC_{T}. Since iOpiOp is linearized between CTC_{T} and CC, it follows from Property 2(c) that pOppOp has a candidate return value that is at least xx. This contradicts Property 1. Suppose iOpiOp is linearized before CTC_{T}. If xSx\in S in all configurations from the linearization point of iOpiOp to CTC_{T}, then Property 3 states there is a key xx^{\prime} that is a candidate return value of pOppOp, where w<xx<yw<x\leq x^{\prime}<y. This contradicts Property 1. So xSx\notin S in some configuration between the linearization point of iOpiOp and CTC_{T}. Since xSx\in S in all configurations from the linearization point of iOpiOp to CC, it follows that xx is removed from SS by a Delete(x)(x) operation dOpdOp linearized sometime between CC and CTC_{T}. By Property 2(b), pOppOp has a candidate return value that is at least xx. This contradicts Property 1. Therefore, in any case, ww is the predecessor of yy in CC. ∎

5.3.4 Our Implementation Satisfies Property 1

Recall that our implementation of Predecessor(y)(y) performs a single instance of PredHelper(y)(y), and then returns the result of this instance. The candidate return values of a Predecessor(y)(y) operation are equal to the candidate return values determined by its instance of PredHelper(y)(y). We prove that the candidate return values determined by an instance of PredHelper(y)(y) satisfy the properties, and hence are also satisfied by the Predecessor(y)(y) operation that invoked it. We prove properties about PredHelper(y)(y) because it is also used by Delete(y)(y) operations when performing embedded predecessor operations.

For the remainder of Section 5.3, we let α\alpha be an arbitrary execution of our implementation, and let pOppOp be an arbitrary completed instance of PredHelper(y)(y) in α\alpha. Our proof is by induction on the order in which instances of PredHelper are completed in α\alpha. In particular, we assume that all instances of PredHelper that are completed before pOppOp in α\alpha satisfy Property 1, Property 2, and Property 3.

Let 𝑝𝑁𝑜𝑑𝑒\mathit{pNode} be the predecessor node created by pOppOp. We let I𝑟𝑢𝑎𝑙𝑙I_{\mathit{ruall}}, I𝑢𝑎𝑙𝑙I_{\mathit{uall}}, I𝑛𝑜𝑡𝑖𝑓𝑦I_{\mathit{notify}}, D𝑟𝑢𝑎𝑙𝑙D_{\mathit{ruall}}, D𝑢𝑎𝑙𝑙D_{\mathit{uall}}, and D𝑛𝑜𝑡𝑖𝑓𝑦D_{\mathit{notify}} be the sets of update nodes corresponding to pOppOp’s local variables with the same name. Recall that I𝑟𝑢𝑎𝑙𝑙I_{\mathit{ruall}} and D𝑟𝑢𝑎𝑙𝑙D_{\mathit{ruall}} are update nodes obtained from pOppOp’s traversal of the RU-ALL, I𝑢𝑎𝑙𝑙I_{\mathit{uall}} and D𝑢𝑎𝑙𝑙D_{\mathit{uall}} are update nodes obtained from pOppOp’s traversal of the U-ALL, and I𝑛𝑜𝑡𝑖𝑓𝑦I_{\mathit{notify}} and D𝑛𝑜𝑡𝑖𝑓𝑦D_{\mathit{notify}} are update nodes obtained from pOppOp’s traversal of its notify list (i.e. 𝑝𝑁𝑜𝑑𝑒.𝑛𝑜𝑡𝑖𝑓𝑦𝐿𝑖𝑠𝑡\mathit{pNode}.\mathit{notifyList}). Recall that the keys of update nodes in I𝑢𝑎𝑙𝑙I𝑛𝑜𝑡𝑖𝑓𝑦(D𝑢𝑎𝑙𝑙D𝑟𝑢𝑎𝑙𝑙)(D𝑛𝑜𝑡𝑖𝑓𝑦D𝑟𝑢𝑎𝑙𝑙)I_{\mathit{uall}}\cup I_{\mathit{notify}}\cup(D_{\mathit{uall}}-D_{\mathit{ruall}})\cup(D_{\mathit{notify}}-D_{\mathit{ruall}}) are candidate return values of pOppOp.

There may be one additional candidate return value from pOppOp’s traversal of the relaxed binary trie. When pOppOp’s traversal of the relaxed binary trie returns a value p0p_{0}\neq\bot, p0p_{0} is a candidate return value. If the traversal of the relaxed binary trie returns p0p_{0}\neq\bot and D𝑟𝑢𝑎𝑙𝑙D_{\mathit{ruall}}\neq\emptyset, then the value pOppOp computes for p0p_{0} from lines 217 to 237 is a candidate return value.

It is easy to show that our algorithm satisfies Property 1.

Lemma 5.11.

All candidate return values of pOppOp are less than yy, and pOppOp returns its largest candidate return value.

Proof.

The maximum candidate return value of pOppOp is returned on line 238. It is either a key of an update node in I𝑢𝑎𝑙𝑙I𝑛𝑜𝑡𝑖𝑓𝑦(D𝑢𝑎𝑙𝑙D𝑟𝑢𝑎𝑙𝑙)(D𝑛𝑜𝑡𝑖𝑓𝑦D𝑟𝑢𝑎𝑙𝑙)I_{\mathit{uall}}\cup I_{\mathit{notify}}\cup(D_{\mathit{uall}}-D_{\mathit{ruall}})\cup(D_{\mathit{notify}}-D_{\mathit{ruall}}), or pOppOp’s local variable p0p_{0}.

The update nodes in I𝑢𝑎𝑙𝑙I_{\mathit{uall}} and D𝑢𝑎𝑙𝑙D_{\mathit{uall}} are those returned by TraverseUAll(y)(y) on line 203. By the check on line 126, these update nodes have key less yy. Update nodes in D𝑢𝑎𝑙𝑙D_{\mathit{uall}} and D𝑛𝑜𝑡𝑖𝑓𝑦D_{\mathit{notify}} have keys less than yy by the check in the while loop on line 205. By the specification of RelaxedPredecessor(y)(y), the value p0p_{0} returned by RelaxedPredecessor(y)(y) is either less than yy, or \bot.

When RelaxedPredecessor(y)(y) returns \bot, p0p_{0} is calculated from the return values of the embedded predecessors of DEL nodes in D𝑟𝑢𝑎𝑙𝑙D_{\mathit{ruall}}, or from the keys of update nodes in a list LL. DEL nodes in D𝑟𝑢𝑎𝑙𝑙D_{\mathit{ruall}} have keys less than yy according to line 250. The embedded predecessors of nodes in D𝑟𝑢𝑎𝑙𝑙D_{\mathit{ruall}} are the return values of completed instances of PredHelper(x)(x), for some key in x<yx<y. By assumption that all completed instances of PredHelper satisfy Property 1, PredHelper(x)(x) returns a value less than xx, which is also less than yy. Only notifications of these embedded predecessors with keys less than yy are considered (on lines 222 and 225). So p0p_{0} is a value less than yy. It follows from the code on lines 222 and line 225 that the update nodes that added to LL have keys less than yy. So all candidate return values of pOppOp are less than yy. ∎

5.3.5 Our Implementation Satisfies Property 2

We next define several configurations during pOppOp that are used for the remainder of Section 5.3. Recall that during TraverseRUAll, pOppOp traverses the RU-ALL by atomically reading the next update node in the list and writing that pointer into 𝑝𝑁𝑜𝑑𝑒.𝑅𝑢𝑎𝑙𝑙𝑃𝑜𝑠𝑖𝑡𝑖𝑜𝑛\mathit{pNode}.\mathit{RuallPosition}. For any key xUx\in U, we let C<xC_{<x} be the configuration immediately after pOppOp first atomically reads a pointer of an update node with key less than xx and writes it into 𝑝𝑁𝑜𝑑𝑒.𝑅𝑢𝑎𝑙𝑙𝑃𝑜𝑠𝑖𝑡𝑖𝑜𝑛\mathit{pNode}.\mathit{RuallPosition} during TraverseRUAll. Let CxC_{\leq x} be the configuration immediately after pOppOp first atomically reads a pointer to an update node with key less than or equal to xx and writes it into 𝑝𝑁𝑜𝑑𝑒.𝑅𝑢𝑎𝑙𝑙𝑃𝑜𝑠𝑖𝑡𝑖𝑜𝑛\mathit{pNode}.\mathit{RuallPosition} during TraverseRUAll. Let CTC_{T} be the configuration immediately before pOppOp starts its traversal of the relaxed binary trie. We let C𝑛𝑜𝑡𝑖𝑓𝑦C_{\mathit{notify}} be the configuration immediately after pOppOp reads the head pointer to the first notify node in 𝑝𝑁𝑜𝑑𝑒.𝑛𝑜𝑡𝑖𝑓𝑦𝐿𝑖𝑠𝑡\mathit{pNode}.\mathit{notifyList} (on line 205). A notify node is seen by pOppOp when it traverses 𝑝𝑁𝑜𝑑𝑒.𝑛𝑜𝑡𝑖𝑓𝑦𝐿𝑖𝑠𝑡\mathit{pNode}.\mathit{notifyList} (on line 205) if and only if it is added into 𝑝𝑁𝑜𝑑𝑒.𝑛𝑜𝑡𝑖𝑓𝑦𝐿𝑖𝑠𝑡\mathit{pNode}.\mathit{notifyList} before C𝑛𝑜𝑡𝑖𝑓𝑦C_{\mathit{notify}}.

Because RU-ALL is a linked list of update nodes whose keys are in non-increasing order, C<xC_{<x} occurs at or before CwC_{\leq w} for any two keys w<xw<x. Likewise, CwC_{\leq w} occurs at or before C<wC_{<w}. Since pOppOp performs TraverseRUAll before the start of its traversal of the relaxed binary trie, it follows that for any key xx, C<xC_{<x} occurs before CTC_{T}. Since pOppOp performs its traversal of the relaxed binary trie before it reads the head pointer to the first notify node in 𝑝𝑁𝑜𝑑𝑒.𝑛𝑜𝑡𝑖𝑓𝑦𝐿𝑖𝑠𝑡\mathit{pNode}.\mathit{notifyList}, CTC_{T} occurs before C𝑛𝑜𝑡𝑖𝑓𝑦C_{\mathit{notify}}. This is summarized in the following observation.

Observation 5.12.

Let ww and xx be any two keys in UU where w<xw<x. Then the following statements hold:

  1. (a)

    C<xC_{<x} occurs at or before CwC_{\leq w}.

  2. (b)

    CwC_{\leq w} occurs at or before C<wC_{<w}.

  3. (c)

    C<wC_{<w} occurs before CTC_{T}.

  4. (d)

    CTC_{T} occurs before C𝑛𝑜𝑡𝑖𝑓𝑦C_{\mathit{notify}}.

We next prove several lemmas that say pOppOp’s candidate return values are in SS sometime during its execution interval. This is Property 2(a). We consider several cases, depending on how pOppOp learns about its candidate return value.

Recall that pOppOp traverses the U-ALL during TraverseUall, which returns two sets of update nodes, I𝑢𝑎𝑙𝑙I_{\mathit{uall}} and D𝑢𝑎𝑙𝑙D_{\mathit{uall}}. The next lemma states that the INS nodes in I𝑢𝑎𝑙𝑙I_{\mathit{uall}} returned by TraverseUall have keys in SS sometime during the traversal, while the DEL nodes in D𝑢𝑎𝑙𝑙D_{\mathit{uall}} have keys not in SS sometime during the traversal. This is because an update node 𝑢𝑁𝑜𝑑𝑒\mathit{uNode} is only added to I𝑢𝑎𝑙𝑙I_{\mathit{uall}} or D𝑢𝑎𝑙𝑙D_{\mathit{uall}} after pOppOp checks that 𝑢𝑁𝑜𝑑𝑒\mathit{uNode} is the first activated update node in latest[𝑢𝑁𝑜𝑑𝑒.𝑘𝑒𝑦]\textit{latest}[\mathit{uNode}.\mathit{key}], which determines whether or not 𝑢𝑁𝑜𝑑𝑒.𝑘𝑒𝑦\mathit{uNode}.\mathit{key} is in SS.

Lemma 5.13.

For each 𝑢𝑁𝑜𝑑𝑒I𝑢𝑎𝑙𝑙D𝑢𝑎𝑙𝑙\mathit{uNode}\in I_{\mathit{uall}}\cup D_{\mathit{uall}}, there is a configuration CC during pOppOp’s traversal of the U-ALL (in its instance of TraverseUall) in which 𝑢𝑁𝑜𝑑𝑒\mathit{uNode} is the first activated update node in latest[𝑢𝑁𝑜𝑑𝑒.𝑘𝑒𝑦]\textit{latest}[\mathit{uNode}.\mathit{key}]. Furthermore, CC occurs before pOppOp encounters any update nodes with key greater than 𝑢𝑁𝑜𝑑𝑒.𝑘𝑒𝑦\mathit{uNode}.\mathit{key} during this traversal of U-ALL.

Proof.

Consider an update node 𝑢𝑁𝑜𝑑𝑒I𝑢𝑎𝑙𝑙D𝑢𝑎𝑙𝑙\mathit{uNode}\in I_{\mathit{uall}}\cup D_{\mathit{uall}}. It follows from the code that there is an iteration during TraverseUall where FirstActivated(𝑢𝑁𝑜𝑑𝑒)=True\textsc{FirstActivated}(\mathit{uNode})=\textsc{True} on line 127. By Lemma 5.4, there is a configuration CC during this instance of FirstActivated in which 𝑢𝑁𝑜𝑑𝑒\mathit{uNode} is the first activated update node in latest[𝑢𝑁𝑜𝑑𝑒.key]\textit{latest}[\mathit{uNode}.key]. Since U-ALL is sorted by increasing key, CC occurs before pOppOp encounters any update nodes with key greater than 𝑢𝑁𝑜𝑑𝑒.𝑘𝑒𝑦\mathit{uNode}.\mathit{key} during this instance of TraverseUall.

An update operation that notifies pOppOp about 𝑖𝑁𝑜𝑑𝑒I𝑛𝑜𝑡𝑖𝑓𝑦\mathit{iNode}\in I_{\mathit{notify}} with key xx may be from the Insert(x)(x) operation that created 𝑖𝑁𝑜𝑑𝑒\mathit{iNode}. Alternatively, it may be from an Insert(w)(w) or Delete(w)(w) operation, uOpuOp, for some w<xw<x, that included 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} when it notified pOppOp. This happens because 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} has the largest key less than yy among the INS nodes returned by uOpuOp’s instance of TraverseUall on line 133.

We first handle the case the where the Insert(x)(x) operation that created 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} notifies pOppOp. We show that xSx\in S some time after pOppOp begins accepting notifications with key xx, but before pOppOp begins collecting its notifications. Intuitively, xSx\in S because an update operation verifies its update node is still the first activated node in latest[x]\textit{latest}[x] prior to notifying pOppOp.

Lemma 5.14.

Consider an INS node 𝑖𝑁𝑜𝑑𝑒I𝑛𝑜𝑡𝑖𝑓𝑦\mathit{iNode}\in I_{\mathit{notify}} with key xx. Suppose the Insert(x)(x) operation that created 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} notified pOppOp. Then there is a configuration between C<xC_{<x} and C𝑛𝑜𝑡𝑖𝑓𝑦C_{\mathit{notify}} in which xSx\in S.

Proof.

Let iOpiOp be the Insert(x)(x) operation that created 𝑖𝑁𝑜𝑑𝑒\mathit{iNode}. The INS node 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} can be added to I𝑛𝑜𝑡𝑖𝑓𝑦I_{\mathit{notify}} on line 208 because iOpiOp notifies pOppOp about its own operation, or on line 213 if some other update operation includes 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} in its notify node because 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} has the largest key less than yy among the INS nodes returned by its instance of TraverseUall on line 133.

Suppose 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} is added to I𝑛𝑜𝑡𝑖𝑓𝑦I_{\mathit{notify}} on line 208. So iOpiOp or a Delete(x)(x) operation helping iOpiOp notified pOppOp by adding a notify node 𝑛𝑁𝑜𝑑𝑒\mathit{nNode} into 𝑝𝑁𝑜𝑑𝑒.𝑛𝑜𝑡𝑖𝑓𝑦𝐿𝑖𝑠𝑡\mathit{pNode}.\mathit{notifyList} where 𝑛𝑁𝑜𝑑𝑒.𝑢𝑝𝑑𝑎𝑡𝑒𝑁𝑜𝑑𝑒=𝑖𝑁𝑜𝑑𝑒\mathit{nNode}.\mathit{updateNode}=\mathit{iNode}. Let uOpuOp be the update operation that successfully added 𝑛𝑁𝑜𝑑𝑒\mathit{nNode} into 𝑝𝑁𝑜𝑑𝑒.𝑛𝑜𝑡𝑖𝑓𝑦𝐿𝑖𝑠𝑡\mathit{pNode}.\mathit{notifyList} using CAS on line 147. In the line of code prior, uOpuOp successfully checks that 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} is the first activated update node in latest[x]\textit{latest}[x] during FirstActivated(𝑖𝑁𝑜𝑑𝑒)(\mathit{iNode}) on line 146. By Lemma 5.4, there is a configuration CC during FirstActivated(𝑖𝑁𝑜𝑑𝑒)(\mathit{iNode}) in which xSx\in S. Furthermore, xSx\in S in all configurations from when uOpuOp is linearized to CC. In particular, xSx\in S in the configuration uOpuOp reads that 𝑝𝑁𝑜𝑑𝑒\mathit{pNode} is in P-ALL on line 134. Since pOppOp is active when it has a predecessor node in U-ALL, xSx\in S sometime during pOppOp and when uOpuOp is traversing P-ALL.

Finally, since 𝑖𝑁𝑜𝑑𝑒I𝑛𝑜𝑡𝑖𝑓𝑦\mathit{iNode}\in I_{\mathit{notify}}, this means that when uOpuOp reads 𝑝𝑁𝑜𝑑𝑒.𝑅𝑢𝑎𝑙𝑙𝑃𝑜𝑠𝑖𝑡𝑖𝑜𝑛\mathit{pNode}.\mathit{RuallPosition} on line 140, 𝑝𝑁𝑜𝑑𝑒.𝑅𝑢𝑎𝑙𝑙𝑃𝑜𝑠𝑖𝑡𝑖𝑜𝑛\mathit{pNode}.\mathit{RuallPosition} is a pointer to an update node with key less than xx, and hence after C<xC_{<x}. This read occurs before uOpuOp’s instance of FirstActivated(𝑖𝑁𝑜𝑑𝑒)(\mathit{iNode}), and hence before CC. So CC occurs sometime after C<xC_{<x}. ∎

We next handle the case where an Insert(w)(w) or Delete(w)(w) operation notifies pOppOp about an INS node it did not create.

Lemma 5.15.

Consider an INS node 𝑖𝑁𝑜𝑑𝑒I𝑛𝑜𝑡𝑖𝑓𝑦\mathit{iNode}\in I_{\mathit{notify}} with key xx. Suppose an update operation with key ww notified pOppOp about 𝑖𝑁𝑜𝑑𝑒\mathit{iNode}, where w<x<yw<x<y. Then there is a configuration between C<xC_{<x} and C𝑛𝑜𝑡𝑖𝑓𝑦C_{\mathit{notify}} in which xSx\in S.

Proof.

From the code, 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} is added to I𝑛𝑜𝑡𝑖𝑓𝑦I_{\mathit{notify}} on line 213. So there is a notify node 𝑛𝑁𝑜𝑑𝑒\mathit{nNode} in 𝑝𝑁𝑜𝑑𝑒.𝑛𝑜𝑡𝑖𝑓𝑦𝐿𝑖𝑠𝑡\mathit{pNode}.\mathit{notifyList} where 𝑖𝑁𝑜𝑑𝑒=𝑛𝑁𝑜𝑑𝑒.𝑢𝑝𝑑𝑎𝑡𝑒𝑁𝑜𝑑𝑒𝑀𝑎𝑥\mathit{iNode}=\mathit{nNode}.\mathit{updateNodeMax}. Let uOpuOp be the update node that created 𝑛𝑁𝑜𝑑𝑒\mathit{nNode}, and hence is the update operation that notified pOppOp about 𝑖𝑁𝑜𝑑𝑒\mathit{iNode}. Let 𝑢𝑁𝑜𝑑𝑒\mathit{uNode} be the update node created by uOpuOp.

By the code on line 213, 𝑢𝑁𝑜𝑑𝑒I𝑟𝑢𝑎𝑙𝑙D𝑟𝑢𝑎𝑙𝑙\mathit{uNode}\notin I_{\mathit{ruall}}\cup D_{\mathit{ruall}}. If uOpuOp is a DEL operation, it follows by Lemma 5.16 uOpuOp is linearized sometime after CwC_{\leq w}, and hence after CxC_{\leq x}. If uOpuOp is an Insert operation, then 𝑢𝑁𝑜𝑑𝑒\mathit{uNode} is the first activated update node in latest[w]\textit{latest}[w] from when it was linearized to when it notified pOppOp, which is after pOppOp completes its traversal of the RU-ALL because 𝑛𝑁𝑜𝑑𝑒.𝑛𝑜𝑡𝑖𝑓𝑦𝑇ℎ𝑟𝑒𝑠ℎ𝑜𝑙𝑑=1\mathit{nNode}.\mathit{notifyThreshold}=-1. So it is linearized sometime after CxC_{\leq x}, otherwise 𝑢𝑁𝑜𝑑𝑒I𝑟𝑢𝑎𝑙𝑙\mathit{uNode}\in I_{\mathit{ruall}}. In either case, uOpuOp is linearized sometime after CxC_{\leq x}.

By definition, 𝑖𝑁𝑜𝑑𝑒=𝑛𝑁𝑜𝑑𝑒.𝑢𝑝𝑑𝑎𝑡𝑒𝑁𝑜𝑑𝑒𝑀𝑎𝑥\mathit{iNode}=\mathit{nNode}.\mathit{updateNodeMax} is the update node with largest key less than yy returned by uOpuOp’s instance of TraverseUall on line 133, which occurs sometime between CxC_{\leq x} and C𝑛𝑜𝑡𝑖𝑓𝑦C_{\mathit{notify}}. By Lemma 5.13, there is a configuration CC during this instance of TraverseUall in which 𝑖𝑁𝑜𝑑𝑒.𝑘𝑒𝑦S\mathit{iNode}.\mathit{key}\in S. So CC is between C<xC_{<x} and C𝑛𝑜𝑡𝑖𝑓𝑦C_{\mathit{notify}}. ∎

Recall that prior to traversing the relaxed binary trie, an instance pOppOp of PredHelper first traverses the RU-ALL to find DEL nodes of Delete operations that may have been linearized before the start of pOppOp.

Suppose 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} is the DEL node of a Delete operation with key less than yy that is linearized before pOppOp. If pOppOp does not encounter 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} when it traverses the RU-ALL, then 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} was removed from RU-ALL before pOppOp could encounter it. In this case, pOppOp will also not accept any notifications about 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} and pOppOp will not encounter 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} in the U-ALL. This is formalized in the next lemma.

Lemma 5.16.

Let dOpdOp be an SS-modifying Delete(x)(x) operation for some key x<yx<y, and let 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} be the DEL node created by dOpdOp. If dOpdOp is linearized before CxC_{\leq x}, then either 𝑑𝑁𝑜𝑑𝑒D𝑟𝑢𝑎𝑙𝑙\mathit{dNode}\in D_{\mathit{ruall}} or 𝑑𝑁𝑜𝑑𝑒D𝑢𝑎𝑙𝑙D𝑛𝑜𝑡𝑖𝑓𝑦\mathit{dNode}\notin D_{\mathit{uall}}\cup D_{\mathit{notify}}.

Proof.

Since dOpdOp is linearized before CxC_{\leq x}, 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} is in RU-ALL before CxC_{\leq x}. Suppose pOppOp does not encounter 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} when it traverses the RU-ALL. So dOpdOp removed its 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} from the RU-ALL before pOppOp sets 𝑝𝑁𝑜𝑑𝑒.𝑅𝑢𝑎𝑙𝑙𝑃𝑜𝑠𝑖𝑡𝑖𝑜𝑛\mathit{pNode}.\mathit{RuallPosition} to an update node with key less than to xx, and hence before C<xC_{<x}. Hence, dOpdOp adds a notify node 𝑛𝑁𝑜𝑑𝑒\mathit{nNode} to 𝑝𝑁𝑜𝑑𝑒.𝑛𝑜𝑡𝑖𝑓𝑦𝐿𝑖𝑠𝑡\mathit{pNode}.\mathit{notifyList} with 𝑛𝑁𝑜𝑑𝑒.𝑛𝑜𝑡𝑖𝑓𝑦𝑇ℎ𝑟𝑒𝑠ℎ𝑜𝑙𝑑x\mathit{nNode}.\mathit{notifyThreshold}\geq x. By line 210, 𝑑𝑁𝑜𝑑𝑒D𝑟𝑢𝑎𝑙𝑙\mathit{dNode}\in D_{\mathit{ruall}}.

Suppose pOppOp encounters 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} when it traverses the RU-ALL and pOppOp’s instance of FirstActivated(𝑑𝑁𝑜𝑑𝑒)(\mathit{dNode}) on line 251 returns True. Then 𝑑𝑁𝑜𝑑𝑒D𝑟𝑢𝑎𝑙𝑙\mathit{dNode}\in D_{\mathit{ruall}}.

So pOppOp’s instance of FirstActivated(𝑑𝑁𝑜𝑑𝑒)(\mathit{dNode}) on line 251 returns False. By Lemma 5.5, there is a configuration CC during this instance of FirstActivated(𝑑𝑁𝑜𝑑𝑒)(\mathit{dNode}) in which 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} is not the first activated update node in latest[x]\textit{latest}[x]. So there is an Insert(x)(x) operation linearized sometime between when dOpdOp is linearized and CC, and hence before pOppOp completes TraverseRUAll. Since 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} is no longer the first activated update node in latest[x]\textit{latest}[x], 𝑑𝑁𝑜𝑑𝑒D𝑢𝑎𝑙𝑙\mathit{dNode}\notin D_{\mathit{uall}}. Furthermore, it is before pOppOp sets 𝑝𝑁𝑜𝑑𝑒.𝑅𝑢𝑎𝑙𝑙𝑃𝑜𝑠𝑖𝑡𝑖𝑜𝑛\mathit{pNode}.\mathit{RuallPosition} to an update node with key less than to xx, so 𝑑𝑁𝑜𝑑𝑒D𝑛𝑜𝑡𝑖𝑓𝑦\mathit{dNode}\notin D_{\mathit{notify}}. ∎

The next lemma states that the key of each DEL node 𝑑𝑁𝑜𝑑𝑒D𝑢𝑎𝑙𝑙D𝑟𝑢𝑎𝑙𝑙\mathit{dNode}\in D_{\mathit{uall}}-D_{\mathit{ruall}} is in SS sometime during pOppOp. Likewise, the lemma following it states that the key of each DEL node 𝑑𝑁𝑜𝑑𝑒D𝑛𝑜𝑡𝑖𝑓𝑦D𝑟𝑢𝑎𝑙𝑙\mathit{dNode}\in D_{\mathit{notify}}-D_{\mathit{ruall}} is in SS sometime during pOppOp. Both of these results use Lemma 5.16 to argue that the Delete(x)(x) operation that created 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} was linearized sometime after CxC_{\leq x}. In the configuration immediately before this operation was linearized, xSx\in S.

Lemma 5.17.

Consider a DEL node 𝑑𝑁𝑜𝑑𝑒D𝑢𝑎𝑙𝑙D𝑟𝑢𝑎𝑙𝑙\mathit{dNode}\in D_{\mathit{uall}}-D_{\mathit{ruall}} with key xx. There is a configuration CC after CxC_{\leq x} in which xSx\in S. Furthermore, CC occurs before pOppOp encounters any update nodes with key greater than xx during its traversal of the U-ALL.

Proof.

Let dOpdOp be the creator of 𝑑𝑁𝑜𝑑𝑒\mathit{dNode}. By Lemma 5.13, there is a configuration CC during TraverseUall of pOppOp in which dOpdOp is the latest Delete(x)(x) operation, and hence xSx\notin S. Suppose, for contradiction, that xSx\notin S in all configurations from CxC_{\leq x} to CC. This implies that dOpdOp was linearized before CxC_{\leq x}. By Lemma 5.16, 𝑑𝑁𝑜𝑑𝑒D𝑟𝑢𝑎𝑙𝑙\mathit{dNode}\in D_{\mathit{ruall}} or 𝑑𝑁𝑜𝑑𝑒D𝑢𝑎𝑙𝑙D𝑛𝑜𝑡𝑖𝑓𝑦\mathit{dNode}\notin D_{\mathit{uall}}\cup D_{\mathit{notify}}. This contradicts the fact that 𝑑𝑁𝑜𝑑𝑒D𝑢𝑎𝑙𝑙D𝑟𝑢𝑎𝑙𝑙\mathit{dNode}\in D_{\mathit{uall}}-D_{\mathit{ruall}}. So there exists a configuration after CxC_{\leq x} in which xSx\in S.

Lemma 5.18.

Consider a DEL node 𝑑𝑁𝑜𝑑𝑒D𝑛𝑜𝑡𝑖𝑓𝑦D𝑟𝑢𝑎𝑙𝑙\mathit{dNode}\in D_{\mathit{notify}}-D_{\mathit{ruall}} with key xx. There is a configuration between CxC_{\leq x} and C𝑛𝑜𝑡𝑖𝑓𝑦C_{\mathit{notify}} in which xSx\in S.

Proof.

Let dOpdOp be the Delete(x)(x) operation that created 𝑑𝑁𝑜𝑑𝑒\mathit{dNode}. Since 𝑑𝑁𝑜𝑑𝑒D𝑛𝑜𝑡𝑖𝑓𝑦\mathit{dNode}\in D_{\mathit{notify}}, dOpdOp successfully added a notify node 𝑛𝑁𝑜𝑑𝑒\mathit{nNode} to 𝑝𝑁𝑜𝑑𝑒.𝑛𝑜𝑡𝑖𝑓𝑦𝐿𝑖𝑠𝑡\mathit{pNode}.\mathit{notifyList}, where 𝑛𝑁𝑜𝑑𝑒.𝑛𝑜𝑡𝑖𝑓𝑦𝑇ℎ𝑟𝑒𝑠ℎ𝑜𝑙𝑑<x\mathit{nNode}.\mathit{notifyThreshold}<x. This means that when dOpdOp read 𝑝𝑁𝑜𝑑𝑒.𝑅𝑢𝑎𝑙𝑙𝑃𝑜𝑠𝑖𝑡𝑖𝑜𝑛\mathit{pNode}.\mathit{RuallPosition} on line 140 it pointed to an update node with key less than xx, and FirstActivated(𝑑𝑁𝑜𝑑𝑒)(\mathit{dNode}) on line 135 returned True. By Lemma 5.4, there is a configuration CC during FirstActivated in which 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} is the first activated update node in latest[x]\textit{latest}[x]. Since 𝑑𝑁𝑜𝑑𝑒D𝑟𝑢𝑎𝑙𝑙\mathit{dNode}\notin D_{\mathit{ruall}} and 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} is the first activated update node latest[x]\textit{latest}[x] in CC, it follows by Lemma 5.16 that dOpdOp is linearized sometime after CxC_{\leq x}. In the configuration immediately before dOpdOp is linearized, xSx\in S. ∎

We next focus on proving lemmas about keys of update operations that become candidate return values of pOppOp. They are used to show Property 2(b) and 2(c). At the end of this subsection, we prove that our implementation satisfies Property 2.

The following two technical lemmas state that after an update operation uOpuOp with key xx is linearized, there is an update node with key xx in the U-ALL and is the first activated update node in latest[x]\textit{latest}[x] until some update operation with key xx completes a traversal of the P-ALL, attempting to notify each predecessor node it encounters about uOpuOp. Either uOpuOp performs these notifications itself, or some update operation with key xx helps uOpuOp perform these notifications before linearizing its own operation. The next two lemmas prove this for Insert and Delete operations, respectively.

Lemma 5.19.

Let iOpiOp be an SS-modifying Insert(x)(x) operation for some key w<x<yw<x<y, and let 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} be the INS node created by iOpiOp. For all configurations from when iOpiOp is linearized until some update operation completes an instance of NotifyPredOps(𝑖𝑁𝑜𝑑𝑒)(\mathit{iNode}), 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} is in the U-ALL and is the first activated update node in latest[x]\textit{latest}[x].

Proof.

By definition, iOpiOp is linearized when the status of 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} changes from inactive to active by the CAS on line 119. Immediately after this CAS, 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} is the first activated update node in latest[x]\textit{latest}[x] and is an activated update node in U-ALL. From the code iOpiOp does not remove 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} from U-ALL until after it completes NotifyPredOps(𝑖𝑁𝑜𝑑𝑒)(\mathit{iNode}).

Before any Delete(x)(x) operation dOpdOp is linearized after iOpiOp is linearized, dOpdOp helps perform NotifyPredOps(𝑖𝑁𝑜𝑑𝑒)(\mathit{iNode}) on line 176. Furthermore, iOpiOp does not remove 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} from U-ALL until sometime after it completes its instance of NotifyPredOps(𝑖𝑁𝑜𝑑𝑒)(\mathit{iNode}). It follows that 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} is the first activated update node in latest[x]\textit{latest}[x] and is an activated update node in U-ALL for all configurations starting when iOpiOp is linearized, and ending immediately after some update operation invokes and completes NotifyPredOps(𝑖𝑁𝑜𝑑𝑒)(\mathit{iNode}). ∎

Lemma 5.20.

Let dOpdOp be an SS-modifying Delete(x)(x) operation for some key w<x<yw<x<y, and let 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} be the DEL node created by dOpdOp. For all configurations from when dOpdOp is linearized until either

  • dOpdOp completes an instance of NotifyPredOps(𝑑𝑁𝑜𝑑𝑒)(\mathit{dNode}) on line 188, or

  • an SS-modifying Insert(x)(x) operation is linearized after dOpdOp.

Proof.

By definition, dOpdOp is linearized when it the status of 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} changes from inactive to active by the line 119. Immediately after this CAS, 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} is the first activated update node in latest[x]\textit{latest}[x] and is an activated update node in U-ALL.

Suppose an SS-modifying Insert(x)(x) operation, iOpiOp, is linearized after dOpdOp before dOpdOp invokes and completes NotifyPredOps(𝑑𝑁𝑜𝑑𝑒)(\mathit{dNode}). Only after iOpiOp is linearized is dOpdOp no longer the first activated update node in latest[x]\textit{latest}[x]. Furthermore, dOpdOp does not remove 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} from U-ALL until after it invokes and completes NotifyPredOps(𝑑𝑁𝑜𝑑𝑒)(\mathit{dNode}). Then 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} is the first activated update node in latest[x]\textit{latest}[x] and is an activated update node in U-ALL for all configurations starting when dOpdOp is linearized and ending when iOpiOp is linearized.

So suppose dOpdOp invokes and completes NotifyPredOps(𝑑𝑁𝑜𝑑𝑒)(\mathit{dNode}) before an SS-modifying Insert(x)(x) operation is linearized after dOpdOp. So 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} remains the first activated update node in latest[x]\textit{latest}[x] until after dOpdOp invokes and completes NotifyPredOps(𝑑𝑁𝑜𝑑𝑒)(\mathit{dNode}). Furthermore, dOpdOp does not remove dNodedNode from U-ALL until after it invokes and completes NotifyPredOps(𝑑𝑁𝑜𝑑𝑒)(\mathit{dNode}). Then 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} is the first activated update node in latest[x]\textit{latest}[x] and is an activated update node in U-ALL for all configurations starting when dOpdOp is linearized and ending when iOpiOp is linearized. ∎

Next we prove two lemmas that state different scenarios when an update operation uOpuOp with key xx is linearized during pOppOp, xx is a candidate return value of pOppOp. This is done using the previous two lemmas, which guarantee that an update node with key xx will either be seen when pOppOp traverses the U-ALL, or some update operation with key xx notifies pOppOp sometime before pOppOp completes its traversal of the U-ALL.

Lemma 5.21.

Let dOpdOp be an SS-modifying Delete(x)(x) operation, for some key w<x<yw<x<y, that is linearized sometime between CwC_{\leq w} and CTC_{T}. Then xx is a candidate return value of pOppOp.

Proof.

Suppose some latest update operation uOpuOp^{\prime} with key xx notifies all predecessor nodes in P-ALL before pOppOp completes its traversal of the U-ALL. Note that 𝑝𝑁𝑜𝑑𝑒\mathit{pNode} is inserted into P-ALL before the start of pOppOp, and is not removed from P-ALL until sometime after pOppOp completes its traversal of the U-ALL. So 𝑝𝑁𝑜𝑑𝑒\mathit{pNode} is in P-ALL throughout uOpuOp^{\prime}’s traversal of the P-ALL on line 162. Since uOpuOp^{\prime} is linearized after CwC_{\leq w} it follows that 𝑝𝑁𝑜𝑑𝑒.𝑅𝑢𝑎𝑙𝑙𝑃𝑜𝑠𝑖𝑡𝑖𝑜𝑛\mathit{pNode}.\mathit{RuallPosition} points to an update node with key less than or equal to ww throughout uOpuOp^{\prime} traversal of the P-ALL. So when pOppOp finishes traversing its notify list on line 205, 𝑢𝑁𝑜𝑑𝑒I𝑛𝑜𝑡𝑖𝑓𝑦D𝑛𝑜𝑡𝑖𝑓𝑦\mathit{uNode}\in I_{\mathit{notify}}\cup D_{\mathit{notify}}. Hence, xx is a candidate return value of pOppOp.

So suppose pOppOp completes its traversal of the U-ALL before some latest update operation uOpuOp^{\prime} with key xx notifies all predecessor nodes in P-ALL. Since no latest update operation with key xx notifies pOppOp, no latest update node with key xx is removed from U-ALL before pOppOp completes its traversal of the U-ALL. It follows by Lemma 5.20 and Lemma 5.19 that pOppOp encounters an activated update node 𝑢𝑁𝑜𝑑𝑒\mathit{uNode}^{\prime} with key xx during its traversal of the U-ALL. Since FirstActivated(𝑢𝑁𝑜𝑑𝑒)(\mathit{uNode}^{\prime}) returns True on line 127, 𝑢𝑁𝑜𝑑𝑒I𝑢𝑎𝑙𝑙D𝑢𝑎𝑙𝑙\mathit{uNode}^{\prime}\in I_{\mathit{uall}}\cup D_{\mathit{uall}} when TraverseUall returns on line 203. Hence, xx is a candidate return value of pOppOp. ∎

Lemma 5.22.

Let iOpiOp be an SS-modifying Insert(x)(x) operation, for some key x<yx<y that is linearized sometime after CTC_{T}, but before pOppOp encounters any update nodes with key greater than or equal to xx during its instance of TraverseUall(y)\textsc{TraverseUall}(y). Then xx is a candidate return value of pOppOp.

Proof.

Let 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} be the INS node created by iOpiOp. Suppose some latest update operation notifies all predecessor nodes in P-ALL about 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} before pOppOp completes its traversal of the U-ALL. Note that 𝑝𝑁𝑜𝑑𝑒\mathit{pNode} is inserted into P-ALL before the start of pOppOp, and is not removed from P-ALL until sometime after pOppOp completes its traversal of the U-ALL. So 𝑝𝑁𝑜𝑑𝑒\mathit{pNode} is in P-ALL throughout uOpuOp^{\prime}’s traversal of the P-ALL on line 162. Since uOpuOp^{\prime} is linearized after CwC_{\leq w} it follows that 𝑝𝑁𝑜𝑑𝑒.𝑅𝑢𝑎𝑙𝑙𝑃𝑜𝑠𝑖𝑡𝑖𝑜𝑛\mathit{pNode}.\mathit{RuallPosition} points to an update node with key less than or equal to ww throughout uOpuOp^{\prime} traversal of the P-ALL. So when pOppOp finishes traversing its notify list on line 205, 𝑢𝑁𝑜𝑑𝑒I𝑛𝑜𝑡𝑖𝑓𝑦D𝑛𝑜𝑡𝑖𝑓𝑦\mathit{uNode}\in I_{\mathit{notify}}\cup D_{\mathit{notify}}. Hence, xx is a candidate return value of pOppOp.

So suppose pOppOp completes its traversal of the U-ALL before some latest update operation uOpuOp^{\prime} with key xx notifies all predecessor nodes in P-ALL about about 𝑖𝑁𝑜𝑑𝑒\mathit{iNode}. Since no latest update operation with key xx notifies pOppOp, 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} is not removed from U-ALL before pOppOp completes its traversal of the U-ALL. It follows by Lemma 5.19 that pOppOp encounters an activated update node 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} during its traversal of the U-ALL. Since FirstActivated(𝑢𝑁𝑜𝑑𝑒)(\mathit{uNode}^{\prime}) returns True on line 127, 𝑢𝑁𝑜𝑑𝑒I𝑢𝑎𝑙𝑙D𝑢𝑎𝑙𝑙\mathit{uNode}^{\prime}\in I_{\mathit{uall}}\cup D_{\mathit{uall}} when TraverseUall returns on line 203. Hence, xx is a candidate return value of pOppOp. ∎

The follow lemma describes the scenario in which an Insert(w)(w) or Delete(w)(w) operation, uOpuOp, includes the INS node of an Insert(x)(x) operation it, iOpiOp, for w<x<yw<x<y, when uOpuOp notifies pOppOp. Either iOpiOp will notify pOppOp about its INS node before C𝑛𝑜𝑡𝑖𝑓𝑦C_{\mathit{notify}}, or uOpuOp will see this INS node when it traverses the U-ALL, and hence include an INS node with key at least xx when it notifies pOppOp.

Lemma 5.23.

Let iOpiOp be an SS-modifying Insert(x)(x) operation, for some key x<yx<y. Let uOpuOp be an Insert(w)(w) or Delete(w)(w) operation that notifies pOppOp before C𝑛𝑜𝑡𝑖𝑓𝑦C_{\mathit{notify}}, for some key w<x<yw<x<y. Suppose iOpiOp is linearized after CTC_{T}, but before uOpuOp encounters any update nodes with key greater than or equal to xx during its instance of TraverseUall. Then pOppOp has a candidate return value xx^{\prime}, where w<xx<yw<x\leq x^{\prime}<y.

Proof.

Let 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} be the INS node created by iOpiOp. By Lemma 5.19, 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} is the first activated update node in latest[x]\textit{latest}[x] in all configurations from when iOpiOp is linearized to when it first completes NotifyPredOps(𝑖𝑁𝑜𝑑𝑒)(\mathit{iNode}). It follows by Lemma 5.5 that all instances of FirstActivated(𝑖𝑁𝑜𝑑𝑒)(\mathit{iNode}) during this instance of NotifyPredOps(𝑖𝑁𝑜𝑑𝑒)(\mathit{iNode}) return True. Therefore, if iOpiOp attempts to notify pOppOp before C𝑛𝑜𝑡𝑖𝑓𝑦C_{\mathit{notify}}, iOpiOp successfully notifies pOppOp. Furthermore, iOpiOp notifies pOppOp after CTC_{T}, and hence after C<xC_{<x}. Then 𝑖𝑁𝑜𝑑𝑒I𝑛𝑜𝑡𝑖𝑓𝑦\mathit{iNode}\in I_{\mathit{notify}}. Then pOppOp has a candidate return value xx.

So suppose does not attempt to notify pOppOp before C𝑛𝑜𝑡𝑖𝑓𝑦C_{\mathit{notify}}. Then Lemma 5.19 implies that iOpiOp is the first activated update node in latest[x]\textit{latest}[x] in all configurations from when iOpiOp is linearized to C𝑛𝑜𝑡𝑖𝑓𝑦C_{\mathit{notify}}. Since iOpiOp is linearized before uOpuOp encounters any update nodes with key greater than or equal to xx during its instance of TraverseUall, it follows that uOpuOp encounters 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} in U-ALL. Furthermore, by Lemma 5.5, uOpuOp’s instance of FirstActivated(𝑖𝑁𝑜𝑑𝑒)(\mathit{iNode}) during TraverseUall returns True. This implies that when iOpiOp notifies pOppOp by adding a notify node 𝑛𝑁𝑜𝑑𝑒\mathit{nNode} into 𝑝𝑁𝑜𝑑𝑒.𝑛𝑜𝑡𝑖𝑓𝑦𝐿𝑖𝑠𝑡\mathit{pNode}.\mathit{notifyList}, the 𝑛𝑁𝑜𝑑𝑒.𝑢𝑝𝑑𝑎𝑡𝑒𝑁𝑜𝑑𝑒𝑀𝑎𝑥\mathit{nNode}.\mathit{updateNodeMax} contains a pointer to an INS node with key xx^{\prime}, where w<xx<yw<x\leq x^{\prime}<y. Then pOppOp has a candidate return value xx^{\prime}.

We now show that Property 2 is satisfied for the keys of update nodes in I𝑢𝑎𝑙𝑙I𝑛𝑜𝑡𝑖𝑓𝑦(D𝑢𝑎𝑙𝑙D𝑟𝑢𝑎𝑙𝑙)(D𝑛𝑜𝑡𝑖𝑓𝑦D𝑟𝑢𝑎𝑙𝑙)I_{\mathit{uall}}\cup I_{\mathit{notify}}\cup(D_{\mathit{uall}}-D_{\mathit{ruall}})\cup(D_{\mathit{notify}}-D_{\mathit{ruall}}).

Lemma 5.24.

Let ww be the key of an update node in I𝑢𝑎𝑙𝑙I𝑛𝑜𝑡𝑖𝑓𝑦(D𝑢𝑎𝑙𝑙D𝑟𝑢𝑎𝑙𝑙)(D𝑛𝑜𝑡𝑖𝑓𝑦D𝑟𝑢𝑎𝑙𝑙)I_{\mathit{uall}}\cup I_{\mathit{notify}}\cup(D_{\mathit{uall}}-D_{\mathit{ruall}})\cup(D_{\mathit{notify}}-D_{\mathit{ruall}}), or is the key returned by pOppOp’s instance of RelaxedPredecessor(y)(y). Then there is a configuration CC during pOppOp such that

  1. (a)

    wSw\in S,

  2. (b)

    if CC occurs before CTC_{T} and there exists a Delete(x)(x) operation linearized between CC and CTC_{T} with w<x<yw<x<y, then pOppOp has a candidate return value which is at least xx, and

  3. (c)

    if CC occurs at or after CTC_{T} and there exists an Insert(x)(x) operation linearized between CTC_{T} and CC with w<x<yw<x<y, then pOppOp has a candidate return value which is at least xx.

Proof.

First suppose ww is the key returned by pOppOp’s instance of RelaxedPredecessor(y)(y). By Lemma 4.21, wSw\in S in a configuration CC during RelaxedPredecessor(y)(y). So CC occurs after CTC_{T}. Suppose, for contradiction, that there is an Insert(x)(x) operation, iOpxiOp_{x}, linearized between CTC_{T} and CC, where w<x<yw<x<y. By Lemma 5.22, xx is a candidate return value of pOppOp.

Now suppose ww is the key of an INS node 𝑖𝑁𝑜𝑑𝑒I𝑢𝑎𝑙𝑙\mathit{iNode}\in I_{\mathit{uall}}. Let iOpwiOp_{w} be the Insert(w)(w) operation that created 𝑖𝑁𝑜𝑑𝑒\mathit{iNode}. By Lemma 5.13, there is a configuration CC during pOppOp’s traversal of U-ALL in which wSw\in S. So CC occurs after CTC_{T}. Suppose, for contradiction, that there is an Insert(x)(x) operation, iOpxiOp_{x}, linearized between CTC_{T} and CC, where w<x<yw<x<y. By Lemma 5.22, xx is a candidate return value of pOppOp.

Consider an update node in 𝑢𝑁𝑜𝑑𝑒I𝑛𝑜𝑡𝑖𝑓𝑦(D𝑢𝑎𝑙𝑙D𝑟𝑢𝑎𝑙𝑙)(D𝑛𝑜𝑡𝑖𝑓𝑦D𝑟𝑢𝑎𝑙𝑙)\mathit{uNode}\in I_{\mathit{notify}}\cup(D_{\mathit{uall}}-D_{\mathit{ruall}})\cup(D_{\mathit{notify}}-D_{\mathit{ruall}}), where 𝑢𝑁𝑜𝑑𝑒.𝑘𝑒𝑦=w<y\mathit{uNode}.\mathit{key}=w<y. Suppose there is a configuration CC between CwC_{\leq w} and CTC_{T} in which wSw\in S. Suppose, for contradiction, that there is a Delete(x)(x) operation, dOpxdOp_{x}, linearized between CC and CTC_{T}, where w<x<yw<x<y. By Lemma 5.21, xx is a candidate return value of pOppOp.

So there is no configuration between CwC_{\leq w} and CTC_{T} in which wSw\in S. By Lemma 5.14, Lemma 5.15, Lemma 5.17, and Lemma 5.18, there is a configuration during pOppOp after CwC_{\leq w} in which wSw\in S. This configuration occurs after CTC_{T}.

  • Suppose 𝑢𝑁𝑜𝑑𝑒I𝑛𝑜𝑡𝑖𝑓𝑦\mathit{uNode}\in I_{\mathit{notify}}. Let iOpwiOp_{w} be the Insert(w)(w) operation that created 𝑢𝑁𝑜𝑑𝑒\mathit{uNode}. Let CC be the configuration after CTC_{T} immediately after iOpwiOp_{w} is linearized.

    First, suppose that 𝑢𝑁𝑜𝑑𝑒I𝑛𝑜𝑡𝑖𝑓𝑦\mathit{uNode}\in I_{\mathit{notify}} because iOpwiOp_{w} notified pOppOp. Suppose, for contradiction, that there Insert(x)(x) operation, iOpxiOp_{x}, linearized between CTC_{T} and CC, where w<x<yw<x<y. So iOpxiOp_{x} is linearized before iOpwiOp_{w}. Since iOpwiOp_{w} notifies pOppOp, it follow from Lemma 5.23 that pOppOp has a candidate return value whose value is at least xx.

    Next, suppose 𝑢𝑁𝑜𝑑𝑒I𝑛𝑜𝑡𝑖𝑓𝑦\mathit{uNode}\in I_{\mathit{notify}} because some Insert(w)(w^{\prime}) or Delete(w)(w^{\prime}) operation included 𝑢𝑁𝑜𝑑𝑒\mathit{uNode} in its notification to pOppOp, where w<w<yw^{\prime}<w<y. Suppose, for contradiction, that there Insert(x)(x) operation, iOpxiOp_{x}, linearized between CTC_{T} and CC, where w<x<yw<x<y. Since uOpuOp includes 𝑢𝑁𝑜𝑑𝑒\mathit{uNode} in its notification to pOppOp, this means that FirstActivated(𝑢𝑁𝑜𝑑𝑒)(\mathit{uNode}) returned True during uOpuOp’s instance of TraverseUall. By Lemma 5.4, there is a configuration CC^{\prime} during this instance of FirstActivated(𝑢𝑁𝑜𝑑𝑒)(\mathit{uNode}) in which 𝑢𝑁𝑜𝑑𝑒\mathit{uNode} is the first activated update node in latest[w]\textit{latest}[w], which is after CC. Since U-ALL is sorted by increasing key, CC^{\prime} occurs before uOpuOp encounters any update node with key greater than or equal to xx during its instance of TraverseUall. Since iOpiOp is linearized between CTC_{T} and CC, and hence before CC^{\prime}, it follows by Lemma 5.23 that pOppOp has a candidate return value x,x^{\prime}, where w<xx<yw<x\leq x^{\prime}<y.

  • Suppose 𝑢𝑁𝑜𝑑𝑒(D𝑢𝑎𝑙𝑙D𝑟𝑢𝑎𝑙𝑙)\mathit{uNode}\in(D_{\mathit{uall}}-D_{\mathit{ruall}}). Let dOpwdOp_{w} be the Delete(x)(x) operation that created 𝑢𝑁𝑜𝑑𝑒\mathit{uNode}. Let CC be the configuration immediate before dOpwdOp_{w} is linearized.

    Suppose, for contradiction, that there is a Insert(x)(x) operation, iOpxiOp_{x}, linearized between CTC_{T} and CC, where w<x<yw<x<y. Since 𝑢𝑁𝑜𝑑𝑒(D𝑢𝑎𝑙𝑙D𝑟𝑢𝑎𝑙𝑙)\mathit{uNode}\in(D_{\mathit{uall}}-D_{\mathit{ruall}}) and pOppOp encounters 𝑢𝑁𝑜𝑑𝑒\mathit{uNode} when it traverse U-ALL, CC occurs before pOppOp encounters any update nodes with key greater than ww. It follows by Lemma 5.22 that xx is a candidate return value of pOppOp.

  • Suppose 𝑢𝑁𝑜𝑑𝑒(D𝑛𝑜𝑡𝑖𝑓𝑦D𝑟𝑢𝑎𝑙𝑙)\mathit{uNode}\in(D_{\mathit{notify}}-D_{\mathit{ruall}}). Let dOpwdOp_{w} be the Delete(x)(x) operation that created 𝑢𝑁𝑜𝑑𝑒\mathit{uNode}. Let CC be the configuration immediate before dOpwdOp_{w} is linearized.

    Suppose, for contradiction, that there is a Insert(x)(x) operation, iOpxiOp_{x}, linearized between CTC_{T} and CC, where w<x<yw<x<y. So iOpxiOp_{x} is linearized before iOpwiOp_{w}. Since dOpwdOp_{w} notifies pOppOp, it follow from Lemma 5.23 that pOppOp has a candidate return value whose value is at least xx.

Recall that pOppOp may have one additional candidate return value p0p_{0}. We will show in the next section that this candidate return value is in SS in CTC_{T}, and hence vacuously satisfies Property 2.

5.3.6 Our Implementation Satisfies Property 3

In this section, we show that Property 3 is satisfied by our implementation. It is easy to show this property is satisfied when pOppOp’s instance of RelaxedPredecessor(y)(y) returns a value p0p_{0}\neq\bot. When \bot is returned, we show that after pOppOp completes the pseudocode from lines 217 to 237, if a key xx is completely present throughout pOppOp, then pOppOp sets p0p_{0} to a value at least xx, and hence pOppOp has a candidate return value at least xx.

The next lemma states that if there is a linearized update operation uOpuOp with key xx that has not completed updating the relaxed binary trie is concurrent with pOppOp’s traversal of the relaxed binary trie, pOppOp will encounter an update node with key xx when pOppOp traverses the U-ALL or when pOppOp traverses its own notify list. Intuitively, pOppOp will either traverse the U-ALL before uOpuOp can remove its update node from the U-ALL, or uOpuOp will notify pOppOp before pOppOp removes its predecessor node from the P-ALL.

Lemma 5.25.

Let uOpuOp be a linearized, SS-modifying update operation with key xx, where x<yx<y. Suppose that, at any point during pOppOp’s traversal of the relaxed binary trie, uOpuOp is the latest update operation with key xx and uOpuOp has not yet completed updating the relaxed binary trie. Then I𝑢𝑎𝑙𝑙I𝑛𝑜𝑡𝑖𝑓𝑦D𝑢𝑎𝑙𝑙D𝑛𝑜𝑡𝑖𝑓𝑦I_{\mathit{uall}}\cup I_{\mathit{notify}}\cup D_{\mathit{uall}}\cup D_{\mathit{notify}} contains an update node with key at least xx.

Proof.

Since uOpuOp is an SS-modifying update operation, the update node 𝑢𝑁𝑜𝑑𝑒\mathit{uNode} it created is the first activated update node in latest[x]\textit{latest}[x] when it was linearized. By Lemma 5.19 and Lemma 5.20, before a latest update node with key xx is removed from U-ALL, some latest update node with key xx notifies the predecessor operations whose predecessor nodes are in the P-ALL. We consider two cases, depending on whether pOppOp completes its traversal of the U-ALL on line 203 first, or if some latest update operation with key xx completes notifying pOppOp on line 162 first.

Suppose pOppOp completes its traversal of the U-ALL before some latest update operation with key xx notifies pOppOp. The danger interval of uOpuOp starts before the end of pOppOp’s binary trie traversal, so U-ALL contains a latest update node with key xx before the start of pOppOp’s traversal of the U-ALL. Since no latest update operation with key xx notifies pOppOp, no latest update node with key xx is removed from U-ALL before pOppOp completes its traversal of the U-ALL. So pOppOp encounters a latest update node 𝑢𝑁𝑜𝑑𝑒\mathit{uNode}^{\prime} with key xx during its traversal of the U-ALL. Since FirstActivated(𝑢𝑁𝑜𝑑𝑒)(\mathit{uNode}^{\prime}) returns True on line 127, 𝑢𝑁𝑜𝑑𝑒I𝑢𝑎𝑙𝑙D𝑢𝑎𝑙𝑙\mathit{uNode}^{\prime}\in I_{\mathit{uall}}\cup D_{\mathit{uall}} when TraverseUall returns on line 203.

Suppose some latest update operation uOpuOp^{\prime} with key xx notifies all predecessor nodes in the P-ALL before pOppOp completes its traversal of the U-ALL, and hence before pOppOp removes 𝑝𝑁𝑜𝑑𝑒\mathit{pNode} from P-ALL. Note that since pOppOp starts its binary trie traversal before the end of uOpuOp’s danger interval, 𝑝𝑁𝑜𝑑𝑒\mathit{pNode} is inserted into P-ALL before uOpuOp, and hence before uOpuOp^{\prime} starts its traversal of P-ALL on ine 162 (or line 133). Since 𝑝𝑁𝑜𝑑𝑒\mathit{pNode} is in P-ALL throughout uOpuOp^{\prime}’s traversal of the P-ALL on line 162 uOpuOp^{\prime} notifies pOppOp. Since uOpuOp^{\prime} does not notify pOppOp until after the end of its danger interval and pOppOp completes its traversal of RU-ALL before the start of its traversal of the relaxed binary trie, it follows that 𝑝𝑁𝑜𝑑𝑒.𝑅𝑢𝑎𝑙𝑙𝑃𝑜𝑠𝑖𝑡𝑖𝑜𝑛\mathit{pNode}.\mathit{RuallPosition} points to the sentinel node in the RU-ALL with key -\infty throughout uOpuOp^{\prime} traversal of the P-ALL. So when pOppOp finishes traversing its notify list on line 205, 𝑢𝑁𝑜𝑑𝑒I𝑛𝑜𝑡𝑖𝑓𝑦D𝑛𝑜𝑡𝑖𝑓𝑦\mathit{uNode}\in I_{\mathit{notify}}\cup D_{\mathit{notify}}. ∎

Recall from the specification of RelaxedPredecessor(y)(y) that if it returns \bot, there exists an SS-modifying update operation with key less than yy concurrent with the instance of RelaxedPredecessor(y)(y). The next lemma states that pOppOp will be notified by this update operation or pOppOp will encounter the update node it created when it traverses the U-ALL.

Lemma 5.26.

Let kk be the largest key less than yy that is completely present throughout pOppOp’s traversal of the relaxed binary trie. Suppose pOppOp’s instance of RelaxedPredecessor(y)(y) returns \bot. If I𝑢𝑎𝑙𝑙I𝑛𝑜𝑡𝑖𝑓𝑦(D𝑢𝑎𝑙𝑙D𝑟𝑢𝑎𝑙𝑙)(D𝑛𝑜𝑡𝑖𝑓𝑦D𝑟𝑢𝑎𝑙𝑙)I_{\mathit{uall}}\cup I_{\mathit{notify}}\cup(D_{\mathit{uall}}-D_{\mathit{ruall}})\cup(D_{\mathit{notify}}-D_{\mathit{ruall}}) does not contain an update node with key at least kk, then D𝑟𝑢𝑎𝑙𝑙D_{\mathit{ruall}} contains an update node with key at least kk.

Proof.

By assumption, pOppOp’s instance of RelaxedPredecessor(y)(y) returns \bot. So by Lemma 4.22, there exists an SS-modifying update operation uOpuOp with key xx, where k<x<yk<x<y, such that, at some point during pOppOp’s traversal of the relaxed binary trie, uOpuOp is the latest update operation with key xx and uOpuOp has not yet completed updating the relaxed binary trie. By Lemma 5.25, I𝑢𝑎𝑙𝑙I𝑛𝑜𝑡𝑖𝑓𝑦D𝑢𝑎𝑙𝑙D𝑛𝑜𝑡𝑖𝑓𝑦I_{\mathit{uall}}\cup I_{\mathit{notify}}\cup D_{\mathit{uall}}\cup D_{\mathit{notify}} contains an update node with key xx. Therefore, if I𝑢𝑎𝑙𝑙I𝑛𝑜𝑡𝑖𝑓𝑦(D𝑢𝑎𝑙𝑙D𝑟𝑢𝑎𝑙𝑙)(D𝑛𝑜𝑡𝑖𝑓𝑦D𝑟𝑢𝑎𝑙𝑙)I_{\mathit{uall}}\cup I_{\mathit{notify}}\cup(D_{\mathit{uall}}-D_{\mathit{ruall}})\cup(D_{\mathit{notify}}-D_{\mathit{ruall}}) does not contain an update node with key at least xx, D𝑟𝑢𝑎𝑙𝑙D_{\mathit{ruall}} contains an update node with key xx.

The remaining lemmas relate to the value computed for p0p_{0} from lines 217 to 237. We use the following definitions for all these lemmas. Let kk be the largest key less than yy that is completely present throughout pOppOp. If pOppOp determines a predecessor node 𝑝𝑁𝑜𝑑𝑒\mathit{pNode}^{\prime} on line 221, let CC be the configuration immediately after 𝑝𝑁𝑜𝑑𝑒\mathit{pNode}^{\prime} was announced; otherwise let CC be the configuration immediately after 𝑝𝑁𝑜𝑑𝑒\mathit{pNode} was announced. We let RR and LL refer to pOppOp’s local variables with the same name, and consider their values at various points in the algorithm.

Lemma 5.27.

Suppose I𝑢𝑎𝑙𝑙I𝑛𝑜𝑡𝑖𝑓𝑦(D𝑢𝑎𝑙𝑙D𝑟𝑢𝑎𝑙𝑙)(D𝑛𝑜𝑡𝑖𝑓𝑦D𝑟𝑢𝑎𝑙𝑙)I_{\mathit{uall}}\cup I_{\mathit{notify}}\cup(D_{\mathit{uall}}-D_{\mathit{ruall}})\cup(D_{\mathit{notify}}-D_{\mathit{ruall}}) does not contain an update node with key at least kk. Let kk^{\prime} be any key such that k>kk^{\prime}>k, kRk^{\prime}\in R immediately after line 231, and kSk^{\prime}\in S in some configuration CC^{\prime} after CC. If kk^{\prime} is removed from RR on line 235 in some iteration of the for-loop on line 232, then there exists a key ww, where kw<kk\leq w<k^{\prime}, is added RR on line 235 in the same iteration or later iteration of the for-loop and wSw\in S in some configuration after CC.

Proof.

So suppose kk^{\prime} is removed from RR on line 235 because there is a DEL node, 𝑑𝑁𝑜𝑑𝑒\mathit{dNode}, with 𝑑𝑁𝑜𝑑𝑒.𝑘𝑒𝑦=k\mathit{dNode}.\mathit{key}=k^{\prime}. Let dOpdOp be the Delete(k)(k^{\prime}) operation that created 𝑑𝑁𝑜𝑑𝑒\mathit{dNode}.

Suppose 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} is linearized before CC^{\prime}. Since kCk^{\prime}\in C^{\prime}, there is an SS-modifying Insert(k)(k^{\prime}) operation, iOpiOp, linearized after dOpdOp but before CC^{\prime}. Let 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} be the INS node created by iOpiOp. If iOpiOp does not notify 𝑝𝑁𝑜𝑑𝑒\mathit{pNode}^{\prime} or 𝑝𝑁𝑜𝑑𝑒\mathit{pNode} by the time pOppOp completes its traversal of the U-ALL, then it follows from Lemma 5.19 that 𝑖𝑁𝑜𝑑𝑒I𝑢𝑎𝑙𝑙\mathit{iNode}\in I_{\mathit{uall}}. So iOpiOp must notify 𝑝𝑁𝑜𝑑𝑒\mathit{pNode}^{\prime} or 𝑝𝑁𝑜𝑑𝑒\mathit{pNode} before pOppOp completes its traversal of the U-ALL. Since 𝑖𝑁𝑜𝑑𝑒I𝑢𝑎𝑙𝑙I𝑛𝑜𝑡𝑖𝑓𝑦\mathit{iNode}\notin I_{\mathit{uall}}\cup I_{\mathit{notify}}, if iOpiOp notifies 𝑝𝑁𝑜𝑑𝑒\mathit{pNode}, the notification must be rejected, and hence iOpiOp notified pOppOp when 𝑝𝑁𝑜𝑑𝑒.𝑅𝑢𝑎𝑙𝑙𝑃𝑜𝑠𝑖𝑡𝑖𝑜𝑛\mathit{pNode}.\mathit{RuallPosition} points to an update node with key greater than kk. It follows that 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} is added to L2L_{2} on line 227. Otherwise iOpiOp does not notify 𝑝𝑁𝑜𝑑𝑒\mathit{pNode}, so it notifies 𝑝𝑁𝑜𝑑𝑒\mathit{pNode}^{\prime}. Then 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} is added to L1L_{1}. Since iOpiOp is linearized after dOpdOp, 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} appears after 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} in LL. Then kk^{\prime} is added to RR on line 233 once pOppOp encounters 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} in LL. So kSk^{\prime}\in S sometime after CC.

So 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} is linearized after CC^{\prime}. Then the second embedded predecessor of 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} returns a value ww where kw<kk\leq w<k^{\prime}, and wSw\in S sometime during the embedded predecessor. So wSw\in S after CC^{\prime}. Then when kk^{\prime} is removed from RR on line 235, ww is added to RR on line 235 in the same iteration. ∎

The following lemma is the main lemma that proves Property 3 is satisfied by pOppOp. It ensures that for each key kk inserted into the relaxed binary trie that is not later deleted by the start of pOppOp’s traversal of the relaxed binary trie, pOppOp has a candidate return value at least kk. If key kk is not seen in any of these, then a pOppOp’s traversal of the binary trie could not complete due to Delete operations that may have been linearized before the start of pOppOp (i.e. Delete operations whose DEL nodes are in D𝑟𝑢𝑎𝑙𝑙D_{\mathit{ruall}}). In this case, p0p_{0} is set to a value at least kk on line 237.

Lemma 5.28.

Suppose an SS-modifying Insert(w)(w) operation iOpiOp is linearized before CTC_{T}, w<yw<y, and there are no SS-modifying Delete(w)(w) operations linearized after iOpiOp and before CTC_{T}. Then either

  • pOppOp’s instance of RelaxedPredecessor(y)(y) returns a value at least ww,

  • I𝑢𝑎𝑙𝑙I𝑛𝑜𝑡𝑖𝑓𝑦(D𝑢𝑎𝑙𝑙D𝑟𝑢𝑎𝑙𝑙)(D𝑛𝑜𝑡𝑖𝑓𝑦D𝑟𝑢𝑎𝑙𝑙)I_{\mathit{uall}}\cup I_{\mathit{notify}}\cup(D_{\mathit{uall}}-D_{\mathit{ruall}})\cup(D_{\mathit{notify}}-D_{\mathit{ruall}}) contains an update node with key at least ww, or

  • p0p_{0} is set to a value at least ww on line 237.

Proof.

If I𝑢𝑎𝑙𝑙I𝑛𝑜𝑡𝑖𝑓𝑦(D𝑢𝑎𝑙𝑙D𝑟𝑢𝑎𝑙𝑙)(D𝑛𝑜𝑡𝑖𝑓𝑦D𝑟𝑢𝑎𝑙𝑙)I_{\mathit{uall}}\cup I_{\mathit{notify}}\cup(D_{\mathit{uall}}-D_{\mathit{ruall}})\cup(D_{\mathit{notify}}-D_{\mathit{ruall}}) contains an update node with a key at which is at least ww, the lemma holds. So suppose I𝑢𝑎𝑙𝑙I𝑛𝑜𝑡𝑖𝑓𝑦(D𝑢𝑎𝑙𝑙D𝑟𝑢𝑎𝑙𝑙)(D𝑛𝑜𝑡𝑖𝑓𝑦D𝑟𝑢𝑎𝑙𝑙)I_{\mathit{uall}}\cup I_{\mathit{notify}}\cup(D_{\mathit{uall}}-D_{\mathit{ruall}})\cup(D_{\mathit{notify}}-D_{\mathit{ruall}}) does not contain an update node with key which is at least ww. By Lemma 5.25, iOpiOp’s update of the relaxed binary trie does not overlap with pOppOp’s traversal of the binary trie. So wSw\in S throughout pOppOp’s traversal of the binary trie and ww is completely present through pOppOp’s traversal of the relaxed binary trie.

We prove that the lemma is true for kk, and hence is true for wkw\leq k. By the specification of the relaxed binary tire (Lemma 4.22 and Lemma 4.21), if RelaxedPredecessor(y)(y) returns a value other than \bot, it returns a value at least kk.

So suppose RelaxedPredecessor(y)(y) returns \bot. By Lemma 4.22, there is an SS-modifying update operation, uOpuOp, with key xx, where k<x<yk<x<y whose update to the relaxed binary trie overlaps with pOppOp’s traversal of the relaxed binary trie. Since I𝑢𝑎𝑙𝑙I𝑛𝑜𝑡𝑖𝑓𝑦(D𝑢𝑎𝑙𝑙D𝑟𝑢𝑎𝑙𝑙)(D𝑛𝑜𝑡𝑖𝑓𝑦D𝑟𝑢𝑎𝑙𝑙)I_{\mathit{uall}}\cup I_{\mathit{notify}}\cup(D_{\mathit{uall}}-D_{\mathit{ruall}})\cup(D_{\mathit{notify}}-D_{\mathit{ruall}}) does not contain an update node with key which is at least kk, it follows from Lemma 5.25 that D𝑟𝑢𝑎𝑙𝑙D_{\mathit{ruall}} contains a DEL node with key kk.

So the if-statement of line 217 of PredHelper(y)(y) evaluates to True. Let 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} be the INS node created by iOpiOp. If pOppOp determines a predecessor node 𝑝𝑁𝑜𝑑𝑒\mathit{pNode}^{\prime} on line 221, let CC be the configuration immediately after 𝑝𝑁𝑜𝑑𝑒\mathit{pNode}^{\prime} was announced; otherwise let CC be the configuration immediately after 𝑝𝑁𝑜𝑑𝑒\mathit{pNode} was announced.

Suppose iOpiOp is linearized after CC. If iOpiOp does not notify 𝑝𝑁𝑜𝑑𝑒\mathit{pNode}^{\prime} or 𝑝𝑁𝑜𝑑𝑒\mathit{pNode} by the time pOppOp completes its traversal of the U-ALL, then it follows from Lemma 5.19 that 𝑖𝑁𝑜𝑑𝑒I𝑢𝑎𝑙𝑙\mathit{iNode}\in I_{\mathit{uall}}. Since 𝑖𝑁𝑜𝑑𝑒I𝑢𝑎𝑙𝑙I𝑛𝑜𝑡𝑖𝑓𝑦\mathit{iNode}\notin I_{\mathit{uall}}\cup I_{\mathit{notify}}, if iOpiOp notifies 𝑝𝑁𝑜𝑑𝑒\mathit{pNode}, the notification must be rejected, and hence iOpiOp notified pOppOp when 𝑝𝑁𝑜𝑑𝑒.𝑅𝑢𝑎𝑙𝑙𝑃𝑜𝑠𝑖𝑡𝑖𝑜𝑛\mathit{pNode}.\mathit{RuallPosition} points to an update node with key greater than kk. It follows that 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} is added to L2L_{2} on line 227. Otherwise iOpiOp does not notify 𝑝𝑁𝑜𝑑𝑒\mathit{pNode}, so it notifies 𝑝𝑁𝑜𝑑𝑒\mathit{pNode}^{\prime}. Then 𝑖𝑁𝑜𝑑𝑒\mathit{iNode} is added to L1L_{1}. In any case, kk is a key in RR. So kk is the key of an INS node in LL, and hence is added to RR on line 233. By assumption, there are no Delete(k)(k) operations linearized after iOpiOp and before the end of pOppOp’s traversal of the relaxed binary trie. Since LL only contains the update nodes of update operations linearized before the start of pOppOp’s traversal of the relaxed binary trie, the last update node with key kk in LL is iOpiOp’s INS node. So kk is not removed from RR on line 235.

Furthermore, D𝑟𝑢𝑎𝑙𝑙D_{\mathit{ruall}} only contains the update nodes of update operations linearized before the start of pOppOp’s traversal of the relaxed binary trie. For contradiction, suppose there is a DEL node in D𝑟𝑢𝑎𝑙𝑙D_{\mathit{ruall}} with key kk. Then pOppOp encountered this DEL node in RU-ALL and simultaneously set 𝑝𝑁𝑜𝑑𝑒.𝑅𝑢𝑎𝑙𝑙𝑃𝑜𝑠𝑖𝑡𝑖𝑜𝑛\mathit{pNode}.\mathit{RuallPosition} to point to an update node with key kk. From this point on, pOppOp accepts all notifications from Insert operations with key kk. When pOppOp put this DEL node into D𝑟𝑢𝑎𝑙𝑙D_{\mathit{ruall}}, it was the latest update operation with key kk. Therefore, iOpiOp was linearized after this point. Hence, either iOpiOp notified pOppOp or pOppOp encountered iOpiOp’s INS node when it traversed the U-ALL. This contradicts the fact that 𝑖𝑁𝑜𝑑𝑒I𝑢𝑎𝑙𝑙I𝑛𝑜𝑡𝑖𝑓𝑦\mathit{iNode}\notin I_{\mathit{uall}}\cup I_{\mathit{notify}}. Thus, kk is not removed from RR on line 236.

Now suppose iOpiOp is linearized before CC. So kSk\in S in all configurations between CC and the end of pOppOp’s traversal of the relaxed binary trie. Note that CC occurs before the start of the first embedded predecessor operation of any DEL node in D𝑟𝑢𝑎𝑙𝑙D_{\mathit{ruall}}. Recall that there is a DEL node 𝑑𝑁𝑜𝑑𝑒D𝑟𝑢𝑎𝑙𝑙\mathit{dNode}\in D_{\mathit{ruall}} with key xx such that k<x<yk<x<y. The first embedded predecessor of the Delete(x)(x) operation, dOpxdOp_{x}, that created 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} begins after CC. From the code, this embedded predecessor operation completes before 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} is added to the RU-ALL. Since pOppOp added 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} to D𝑟𝑢𝑎𝑙𝑙D_{\mathit{ruall}} while it traversed the RU-ALL, 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} was added to the RU-ALL before pOppOp began its traversal of the relaxed binary trie. The first embedded predecessor of dOpxdOp_{x} returns a value kk^{\prime} such that kk<xk\leq k^{\prime}<x, because kSk\in S throughout its execution interval. This value will be added to RR on line 231. So RR contains at least one value at least kk at this point.

Since RR contains at least one value at least kk before the for-loop on line 232, the Lemma 5.27 implies that RR contains a value at least kk after the for-loop. Let k′′k^{\prime\prime} be the smallest value k′′kk^{\prime\prime}\geq k that is in RR immediately before line 236 (i.e. immediately after pOppOp completes its local traversal of LL during the for-loop on line 232). Suppose, for contradiction, that k′′k^{\prime\prime} is removed from RR on line 236. Then there exists a DEL node, 𝑑𝑁𝑜𝑑𝑒D𝑟𝑢𝑎𝑙𝑙\mathit{dNode}^{\prime}\in D_{\mathit{ruall}} such that 𝑑𝑁𝑜𝑑𝑒.𝑘𝑒𝑦=k′′\mathit{dNode}^{\prime}.\mathit{key}=k^{\prime\prime}. By definition of CC, its first embedded predecessor occurs after CC. So the first embedded predecessor of 𝑑𝑁𝑜𝑑𝑒\mathit{dNode}^{\prime} returns a key k′′′k^{\prime\prime\prime} where kk′′′<k′′k\leq k^{\prime\prime\prime}<k^{\prime\prime}. Lemma 5.27 implies that, immediately before line 236, RR contains a key at least k′′′k^{\prime\prime\prime}. This contradicts the definition of k′′k^{\prime\prime}.

Therefore, p0p_{0} is set to a value at least kk on line 237. ∎

It remains to prove that, when p0p_{0} is set to a value kk on line 237 and is the largest candidate return value, it satisfies Property 2. We do this by proving kSk\in S in CTC_{T}.

Lemma 5.29.

Suppose p0p_{0} is set to a value kk on line 237 and I𝑢𝑎𝑙𝑙I𝑛𝑜𝑡𝑖𝑓𝑦(D𝑢𝑎𝑙𝑙D𝑟𝑢𝑎𝑙𝑙)(D𝑛𝑜𝑡𝑖𝑓𝑦D𝑟𝑢𝑎𝑙𝑙)I_{\mathit{uall}}\cup I_{\mathit{notify}}\cup(D_{\mathit{uall}}-D_{\mathit{ruall}})\cup(D_{\mathit{notify}}-D_{\mathit{ruall}}) does not contain an update node with key at least kk. Then kSk\in S in CTC_{T}.

Proof.

Suppose, for contradiction, that kSk\notin S in CTC_{T}. Let dOpdOp be the SS-modifying Delete(k)(k) operation that last removed kk from SS prior to CTC_{T}. Let 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} be the DEL node created by dOpdOp. Note that no SS-modifying Insert(k)(k) operation is linearized after dOpdOp but before CTC_{T}. If pOppOp determines a predecessor node 𝑝𝑁𝑜𝑑𝑒\mathit{pNode}^{\prime} on line 221, let CC be the configuration immediately after 𝑝𝑁𝑜𝑑𝑒\mathit{pNode}^{\prime} was announced; otherwise let CC be the configuration immediately after 𝑝𝑁𝑜𝑑𝑒\mathit{pNode} was announced.

Since p0p_{0} is set to a value kk on line 237, kk was previously added to RR on line 233 or line 235. Consider the last time kk is added to RR. We first prove that dOpdOp is linearized after CC.

Suppose p0p_{0} was last set to kk on line 231. Then it is the return value of the first embedded predecessor of some DEL node 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} in D𝑟𝑢𝑎𝑙𝑙D_{\mathit{ruall}}. By the definition of CC, this embedded predecessor is performed after CC. So there exists a configuration after CC and during this embedded predecessor in which kSk\in S. It follows that dOpdOp is linearized after CC.

Suppose p0p_{0} was last set to kk on line 233. Then some SS-modifying Insert(k)(k) operation, iOpiOp, notified 𝑝𝑁𝑜𝑑𝑒\mathit{pNode} or 𝑝𝑁𝑜𝑑𝑒\mathit{pNode}^{\prime}. Furthermore, the INS node, 𝑖𝑁𝑜𝑑𝑒\mathit{iNode}, it created is in the sequence LL. By lemma 5.19, dOpdOp is linearized after iOpiOp notifies 𝑝𝑁𝑜𝑑𝑒\mathit{pNode} or 𝑝𝑁𝑜𝑑𝑒\mathit{pNode}^{\prime}, which is after CC.

Suppose p0p_{0} was last set to kk on line 235. Then it is the return value of the second embedded predecessor of some DEL node 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} in LL, where 𝑑𝑁𝑜𝑑𝑒.𝑘𝑒𝑦R\mathit{dNode}.\mathit{key}\in R. By Lemma 5.27, dOpdOp is linearized after CC.

In any case, dOpdOp is linearized after CC. If dOpdOp is linearized after pOppOp sets 𝑅𝑢𝑎𝑙𝑙𝑃𝑜𝑠𝑖𝑡𝑖𝑜𝑛\mathit{RuallPosition} to point to an update node with key less than kk, then by Lemma 5.21, I𝑢𝑎𝑙𝑙I𝑛𝑜𝑡𝑖𝑓𝑦(D𝑢𝑎𝑙𝑙D𝑟𝑢𝑎𝑙𝑙)(D𝑛𝑜𝑡𝑖𝑓𝑦D𝑟𝑢𝑎𝑙𝑙)I_{\mathit{uall}}\cup I_{\mathit{notify}}\cup(D_{\mathit{uall}}-D_{\mathit{ruall}})\cup(D_{\mathit{notify}}-D_{\mathit{ruall}}) contains contain an update node with key at least kk, a contradiction. So dOpdOp is linearized before pOppOp sets 𝑅𝑢𝑎𝑙𝑙𝑃𝑜𝑠𝑖𝑡𝑖𝑜𝑛\mathit{RuallPosition} to point to an update node with key less than kk. Suppose 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} does not notify 𝑝𝑁𝑜𝑑𝑒\mathit{pNode} or 𝑝𝑁𝑜𝑑𝑒\mathit{pNode}^{\prime} prior to pOppOp setting 𝑅𝑢𝑎𝑙𝑙𝑃𝑜𝑠𝑖𝑡𝑖𝑜𝑛\mathit{RuallPosition} to point to an update node with key less than kk. Then 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} is encountered when pOppOp traverses the RU-ALL, and hence added to D𝑟𝑢𝑎𝑙𝑙D_{\mathit{ruall}}. Then kk is removed from RR on line 236. This contradicts the fact that p0p_{0} is set to a value kk on line 237. So 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} does notify 𝑝𝑁𝑜𝑑𝑒\mathit{pNode} or 𝑝𝑁𝑜𝑑𝑒\mathit{pNode}^{\prime} prior to pOppOp setting 𝑅𝑢𝑎𝑙𝑙𝑃𝑜𝑠𝑖𝑡𝑖𝑜𝑛\mathit{RuallPosition} to point to an update node with key less than kk. Since no SS-modifying Insert(k)(k) operations are linearized after dOpdOp but before CTC_{T}, 𝑑𝑁𝑜𝑑𝑒\mathit{dNode} is the last update node in LL with key kk. It follows that kk is removed from RR on line 235 and not later added back on line 233. This contradicts the fact that p0p_{0} is set to a value kk on line 237. ∎

Therefore, pOppOp’s candidate return values are satisfy all properties. It follows by Theorem 5.10 that if pOppOp returns wU{1}w\in U\cup\{-1\}, then there exists a configuration during pOppOp in which ww is the predecessor of yy. So our implementation of a lock-free binary trie is linearizable with respect to all operations.

5.4 Amortized Analysis

In this section, we give the amortized analysis of our implementation. The amortized analysis is simple because the algorithms to update and traverse the binary trie component of our data structure are wait-free. All other steps traverse and update the lock-free linked lists.

Lemma 5.30.

Search(x)(x) operations have O(1)O(1) worst-case step complexity.

Proof.

Search(x)(x) operations simply find the first activated update node in latest[x]\textit{latest}[x]. From the pseudocode, it always completes in a constant number of reads. ∎

We next consider the amortized step complexity of Insert and Delete operations, uOpuOp, while ignoring the steps taken in embedded Predecessor operations.

Lemma 5.31.

Each Insert and Delete operation, uOpuOp, has O(c˙(uOp)+logu)O(\dot{c}(uOp)+\log u) amortized step complexity, ignoring all instances of NotifyPredOps and PredHelper (i.e. embedded predecessors performed by Delete operations).

Proof.

Let uOpuOp be an Insert and Delete operation. It follows from the psuedocode of InsertBinaryTrie and DeleteBinaryTrie that operations perform a constant number of steps updating each binary trie node on the path from a leaf to the root, which has length log2u+1\lceil\log_{2}u\rceil+1.

Recall that inserting and deleting from a lock-free linked list can be done in amortized step complexity O(c˙(uOp)+L(uOp))O(\dot{c}(uOp)+L(uOp)), where L(uOp)L(uOp) is the number of nodes in the linked list at the start of uOpuOp. The number of nodes in P-ALL, U-ALL, and RU-ALL at the start of uOpuOp is O(c˙(uOp))O(\dot{c}(uOp)). So uOpuOp can update the lock-free linked lists in O(c˙(uOp))O(\dot{c}(uOp)) amortized number of steps. It is known that any execution opαc˙(op)opα2c˙(op)\sum_{op\in\alpha}\dot{c}(op)\leq\sum_{op\in\alpha}2\dot{c}(op). So uOpuOp can traverse P-ALL and U-ALL in O(c˙(op))O(\dot{c}(op)) amortized steps. So uOpuOp can traverse the lock-free linked lists in O(c˙(uOp))O(\dot{c}(uOp)) amortized number of steps. All other parts of Insert and Delete take a constant number of steps. ∎

We next consider the number of steps taken during instances of NotifyPredOps, which involves adding notify nodes into the notify lists of every predecessor node in P-ALL.

Lemma 5.32.

In any execution α\alpha, the total number of steps taken by instances of NotifyPredOps is opαc˙(op)2\sum_{op\in\alpha}\dot{c}(op)^{2}.

Proof.

Let opop be an update operation invoking NotifyPredOps(𝑢𝑁𝑜𝑑𝑒)(\mathit{uNode}). Inserting into the head of 𝑝𝑁𝑜𝑑𝑒.𝑛𝑜𝑡𝑖𝑓𝑦𝐿𝑖𝑠𝑡\mathit{pNode}.\mathit{notifyList} of some predecessor node 𝑝𝑁𝑜𝑑𝑒\mathit{pNode} created by Predecessor operation, pOppOp, takes c˙(C)\dot{c}(C) amortized number of steps, where CC is the configuration immediately after the successful CAS required to update the pointer to add a new notify node. The operation performing the successful CAS pays for the at most c˙(C)1\dot{c}(C)-1 unsuccessful CAS steps it causes. We let opop pay for inserting into 𝑝𝑁𝑜𝑑𝑒.𝑛𝑜𝑡𝑖𝑓𝑦𝐿𝑖𝑠𝑡\mathit{pNode}.\mathit{notifyList} if pOppOp is active at the start of opop. This costs O(c˙(op)2)O(\dot{c}(op)^{2}) amortized number of steps in total since there are at most O(c˙(op))O(\dot{c}(op)) such operations pOppOp. If pOppOp is invoked after the start of opop, pOppOp helps opop pay for inserting into 𝑝𝑁𝑜𝑑𝑒.𝑛𝑜𝑡𝑖𝑓𝑦𝐿𝑖𝑠𝑡\mathit{pNode}.\mathit{notifyList}. There are O(c˙(pOp))O(\dot{c}(pOp)) operations concurrent with pOppOp when it is invoked, so a total of O(c˙(pOp)2)O(\dot{c}(pOp)^{2}) amortized number of steps are charged to pOppOp. Therefore, for any execution α\alpha, the total number of steps taken by instances of NotifyPredOps is opαc˙(op)2\sum_{op\in\alpha}\dot{c}(op)^{2}. ∎

Lemma 5.33.

The amortized number of steps taken by instances pOppOp of PredHelper is O(c˙(pOp)2+c~(pOp)+logu)O(\dot{c}(pOp)^{2}+\tilde{c}(pOp)+\log u).

Proof.

Adding pOppOp’s predecessor node into the P-ALL, which takes O(c˙(pOp))O(\dot{c}(pOp)) amortized number of steps. Performing RelaxedPredecessor(y)(y) takes O(logu)O(\log u) steps in the worst-case. Traversing P-ALL, U-ALL, and RU-ALL takes O(c˙(pOp))O(\dot{c}(pOp)) amortized number of steps. Traversing its own notify list takes O(c¯(pOp))=O(c˙(pOp))O(\bar{c}(pOp))=O(\dot{c}(pOp)) steps because it contains O(c¯(pOp))O(\bar{c}(pOp)) notify nodes. Recall that O(c˙(op)2O(\dot{c}(op)^{2} steps are charged to pOppOp from instances of NotifyPredOps. The steps in this if-block on on line 217 to line 237 involves traversing the 𝑛𝑜𝑡𝑖𝑓𝑦𝐿𝑖𝑠𝑡\mathit{notifyList} of some concurrent Delete operation dOpdOp (on line 222). The length of this 𝑛𝑜𝑡𝑖𝑓𝑦𝐿𝑖𝑠𝑡\mathit{notifyList} is O(c¯(dOp))O(\bar{c}(dOp)). So opop takes O(c~(pOp))O(\tilde{c}(pOp)) steps to traverse this 𝑛𝑜𝑡𝑖𝑓𝑦𝐿𝑖𝑠𝑡\mathit{notifyList}. Furthermore, opop takes O(c¯(pOp))O(\bar{c}(pOp)) steps to traverse the notify list of its own predecessor node on line 225. In summary, the total amortized cost is O(c˙(op)2+c~(op)+logu)O(\dot{c}(op)^{2}+\tilde{c}(op)+\log u). ∎

The total amortized number of steps taken by each operation opop is summarized below.

Theorem 5.34.

We give a linearizable, lock-free implementation of a binary trie for a universe of size uu supporting Search with O(1)O(1) worst-case step complexity, Delete and Predecessor with O(c˙(op)2+c~(op)+logu)O(\dot{c}(op)^{2}+\tilde{c}(op)+\log u) amortized step complexity, and Insert with O(c˙(op)2+logu)O(\dot{c}(op)^{2}+\log u) amortized step complexity.

6 Conclusion and Future Work

The main contribution of our paper is a deterministic, lock-free implementation of a binary trie using read, write, CAS, and AND operations. We prove that the implementation is linearizable. We show that it supports Search with O(1)O(1) worst-case step complexity, Delete and Predecessor with O(c˙(op)2+c~(op)+logu)O(\dot{c}(op)^{2}+\tilde{c}(op)+\log u) amortized step complexity, and Insert with O(c˙(op)2+logu)O(\dot{c}(op)^{2}+\log u) amortized step complexity.

The implementation uses a relaxed binary trie as one of its components. All update operations on the relaxed binary trie take O(logu)O(\log u) steps in the worst-case. Each predecessor operation on the relaxed binary trie takes O(logu)O(\log u) steps in the worst-case, since it can complete without having to help concurrent update operations.

It is possible extend our lock-free binary trie to support Max, which returns the largest key in SS. This can be done by extending the binary trie to represent an additional key \infty that is larger than all keys in UU, and then performing Predecessor()(\infty). By symmetry, Successor and Min can also be supported.

Our lock-free binary trie is in the process of being implemented. The implementation uses a version of epoch-based memory reclamation based on DEBRA [7] to avoid ABA problems when accessing dynamically allocated objects. Its performance will be compared to that of other lock-free data structures supporting Predecessor.

In our lock-free binary trie, predecessor operations get information about update operations that announce themselves in the update announcement linked list. Predecessor operations also announce themselves in the predecessor announcement linked list, so that update update operations can give them information. There is an amortized cost of O(c˙(op)2)O(\dot{c}(op)^{2}) for an update operation, opop, to give information to all predecessor operations. We would like to obtain a more efficient algorithm to do this, which will result in a more efficient implementation of a lock-free binary trie.

A sequential van Emde Boas trie supports Search, Insert, Delete, and Predecessor in O(loglogu)O(\log\log u) worst-case time. We conjecture that there is a lock-free implementation supporting operations with O(c˙(op)2+c~(op)+loglogu)O(\dot{c}(op)^{2}+\tilde{c}(op)+\log\log u) amortized step complexity. Since the challenges are similar, we believe our techniques for implementing a lock-free binary trie will be useful for implementing a lock-free van Emde Boas trie. In particular, using an implementation of a relaxed van Emde Boas trie should be a good approach.

References

  • [1] Maya Arbel-Raviv and Trevor Brown. Reuse, don’t recycle: Transforming lock-free algorithms that throw away descriptors. In 31st International Symposium on Distributed Computing, volume 91 of LIPIcs, pages 4:1–4:16, 2017.
  • [2] Greg Barnes. A method for implementing lock-free shared-data structures. In Lawrence Snyder, editor, Proceedings of the 5th Annual ACM Symposium on Parallel Algorithms and Architectures, SPAA ’93, pages 261–270. ACM, 1993.
  • [3] Paul Bieganski, John Riedl, John V. Carlis, and Ernest F. Retzel. Generalized suffix trees for biological sequence data: Applications and implementation. In 27th Annual Hawaii International Conference on System Sciences (HICSS-27), pages 35–44. IEEE Computer Society, 1994.
  • [4] Guy E. Blelloch and Yuanhao Wei. LL/SC and atomic copy: Constant time, space efficient implementations using only pointer-width CAS. In 34th International Symposium on Distributed Computing, DISC 2020, pages 5:1–5:17, 2020.
  • [5] Anastasia Braginsky and Erez Petrank. A lock-free b+tree. In 24th ACM Symposium on Parallelism in Algorithms and Architectures, SPAA ’12, pages 58–67. ACM, 2012.
  • [6] Trevor Brown. B-slack trees: Space efficient b-trees. In Algorithm Theory - SWAT 2014 - 14th Scandinavian Symposium and Workshops, volume 8503 of Lecture Notes in Computer Science, pages 122–133, 2014.
  • [7] Trevor Brown. Reclaiming memory for lock-free data structures: There has to be a better way. In Proceedings of the 2015 ACM Symposium on Principles of Distributed Computing, PODC 2015, pages 261–270, 2015.
  • [8] Trevor Brown. Techniques for Constructing Efficient Lock-Free Data Structures. PhD thesis, Department of Computer Science, University of Toronto, 2017.
  • [9] Trevor Brown and Hillel Avni. Range queries in non-blocking k-ary search trees. In Principles of Distributed Systems, 16th International Conference, OPODIS 2012, volume 7702 of Lecture Notes in Computer Science, pages 31–45. Springer, 2012.
  • [10] Trevor Brown, Faith Ellen, and Eric Ruppert. Pragmatic primitives for non-blocking data structures. In ACM Symposium on Principles of Distributed Computing, PODC ’13, pages 13–22. ACM, 2013.
  • [11] Trevor Brown, Faith Ellen, and Eric Ruppert. A general technique for non-blocking trees. In Proceedings of the Symposium on Principles and Practice of Parallel Programming (PPoPP), pages 329–342, 2014.
  • [12] Trevor Brown, Aleksandar Prokopec, and Dan Alistarh. Non-blocking interpolation search trees with doubly-logarithmic running time. In PPoPP ’20: 25th ACM SIGPLAN Symposium on Principles and Practice of Parallel Programming, pages 276–291. ACM, 2020.
  • [13] Bapi Chatterjee. Lock-free linearizable 1-dimensional range queries. In Proceedings of the 18th International Conference on Distributed Computing and Networking, page 9. ACM, 2017.
  • [14] Bapi Chatterjee, Nhan Nguyen Dang, and Philippas Tsigas. Efficient lock-free binary search trees. In Proceedings of the ACM Symposium on Principles of Distributed Computing (PODC), pages 322–331, 2014.
  • [15] Mikael Degermark, Andrej Brodnik, Svante Carlsson, and Stephen Pink. Small forwarding tables for fast routing lookups. In Proceedings of the ACM SIGCOMM 1997 Conference on Applications, Technologies, Architectures, and Protocols for Computer Communication, pages 3–14. ACM, 1997.
  • [16] Dana Drachsler, Martin T. Vechev, and Eran Yahav. Practical concurrent binary search trees via logical ordering. In ACM SIGPLAN Symposium on Principles and Practice of Parallel Programming, PPoPP ’14, pages 343–356. ACM, 2014.
  • [17] Faith Ellen, Panagiota Fatourou, Joanna Helga, and Eric Ruppert. The amortized complexity of non-blocking binary search trees. In Proceedings of the Symposium on Principles of Distributed Computing (PODC), pages 332–340, 2014.
  • [18] Faith Ellen, Panagiota Fatourou, Eric Ruppert, and Franck van Breugel. Non-blocking binary search trees. In Proceedings of the 29th Annual ACM Symposium on Principles of Distributed Computing (PODC), pages 131–140, 2010.
  • [19] Panagiota Fatourou, Nikolaos D. Kallimanis, and Eleni Kanellou. An efficient universal construction for large objects. In 23rd International Conference on Principles of Distributed Systems, OPODIS 2019, volume 153 of LIPIcs, pages 18:1–18:15, 2019.
  • [20] Panagiota Fatourou, Elias Papavasileiou, and Eric Ruppert. Persistent non-blocking binary search trees supporting wait-free range queries. In The 31st ACM on Symposium on Parallelism in Algorithms and Architectures, SPAA 2019, pages 275–286. ACM, 2019.
  • [21] Mikhail Fomitchev and Eric Ruppert. Lock-free linked lists and skip lists. In Proceedings of the Twenty-Third Annual ACM Symposium on Principles of Distributed Computing (PODC), pages 50–59, 2004.
  • [22] George Giakkoupis, Mehrdad Jafari Giv, and Philipp Woelfel. Efficient randomized DCAS. In STOC ’21: 53rd Annual ACM SIGACT Symposium on Theory of Computing, pages 1221–1234, 2021.
  • [23] Wojciech M. Golab, Lisa Higham, and Philipp Woelfel. Linearizable implementations do not suffice for randomized distributed computation. In Proceedings of the 43rd ACM Symposium on Theory of Computing, STOC 2011, pages 373–382, 2011.
  • [24] Timothy L. Harris. A pragmatic implementation of non-blocking linked-lists. In Distributed Computing, 15th International Conference, DISC 2001, pages 300–314, 2001.
  • [25] Timothy L. Harris, Keir Fraser, and Ian A. Pratt. A practical multi-word compare-and-swap operation. In Distributed Computing, 16th International Conference, DISC 2002, volume 2508 of Lecture Notes in Computer Science, pages 265–279. Springer, 2002.
  • [26] Maurice Herlihy. Wait-free synchronization. ACM Trans. Program. Lang. Syst., 13(1):124–149, 1991.
  • [27] Maurice Herlihy. A methodology for implementing highly concurrent objects. ACM Trans. Program. Lang. Syst., 15(5):745–770, 1993.
  • [28] Maurice Herlihy and Jeannette M. Wing. Linearizability: A correctness condition for concurrent objects. Trans. Program. Lang. Syst., 12(3):463–492, 1990.
  • [29] Shane V. Howley and Jeremy Jones. A non-blocking internal binary search tree. In 24th ACM Symposium on Parallelism in Algorithms and Architectures, SPAA ’12, pages 161–171. ACM, 2012.
  • [30] Jeremy Ko. The Amortized Analysis of a Non-blocking Chromatic Tree. In 22nd International Conference on Principles of Distributed Systems (OPODIS 2018), volume 125, pages 8:1–8:17, 2018.
  • [31] Miguel A. Martínez-Prieto, Nieves R. Brisaboa, Rodrigo Cánovas, Francisco Claude, and Gonzalo Navarro. Practical compressed string dictionaries. Inf. Syst., 56:73–108, 2016.
  • [32] Aravind Natarajan and Neeraj Mittal. Fast concurrent lock-free binary search trees. In ACM SIGPLAN Symposium on Principles and Practice of Parallel Programming, PPoPP ’14, pages 317–328. ACM, 2014.
  • [33] Rotem Oshman and Nir Shavit. The skiptrie: low-depth concurrent search without rebalancing. In ACM Symposium on Principles of Distributed Computing, PODC ’13, pages 23–32, 2013.
  • [34] Erez Petrank and Shahar Timnat. Lock-free data-structure iterators. In Distributed Computing - 27th International Symposium, DISC 2013, volume 8205 of Lecture Notes in Computer Science, pages 224–238. Springer, 2013.
  • [35] Aleksandar Prokopec, Nathan Grasso Bronson, Phil Bagwell, and Martin Odersky. Concurrent tries with efficient non-blocking snapshots. In Proceedings of the 17th ACM SIGPLAN Symposium on Principles and Practice of Parallel Programming, PPOPP 2012, pages 151–160. ACM, 2012.
  • [36] William W. Pugh. Skip lists: A probabilistic alternative to balanced trees. In Algorithms and Data Structures, Workshop WADS, volume 382 of Lecture Notes in Computer Science, pages 437–449. Springer, 1989.
  • [37] Niloufar Shafiei. Non-blocking patricia tries with replace operations. In IEEE 33rd International Conference on Distributed Computing Systems, ICDCS 2013, pages 216–225. IEEE Computer Society, 2013.
  • [38] Niloufar Shafiei. Non-blocking doubly-linked lists with good amortized complexity. In Proceedings of the 19th International Conference on Principles of Distributed Systems (OPODIS), pages 35:1–35:17, 2015.
  • [39] Ori Shalev and Nir Shavit. Split-ordered lists: lock-free extensible hash tables. In Proceedings of the Twenty-Second ACM Symposium on Principles of Distributed Computing, PODC 2003, pages 102–111. ACM, 2003.
  • [40] John D. Valois. Lock-free linked lists using compare-and-swap. In Proceedings of the Fourteenth Annual ACM Symposium on Principles of Distributed Computing, pages 214–222, 1995.
  • [41] Peter van Emde Boas. Preserving order in a forest in less than logarithmic time and linear space. Inf. Process. Lett., 6(3):80–82, 1977.
  • [42] Peter van Emde Boas, R. Kaas, and E. Zijlstra. Design and implementation of an efficient priority queue. Math. Syst. Theory, 10:99–127, 1977.
  • [43] Yuanhao Wei, Naama Ben-David, Guy E. Blelloch, Panagiota Fatourou, Eric Ruppert, and Yihan Sun. Constant-time snapshots with applications to concurrent data structures. In PPoPP ’21: 26th ACM SIGPLAN Symposium on Principles and Practice of Parallel Programming, pages 31–46, 2021.
  • [44] Dan E. Willard. Log-logarithmic worst-case range queries are possible in space theta(n). Inf. Process. Lett., 17(2):81–84, 1983.