Composable Partitioned Transactions ∗

Twenty years after the initial proposal [4], hardware transactional memory is becoming commonplace. All commercial versions to date—and all that are likely to emerge in the near future—are best effort implementations: a transaction may abort and retry not only because of an actual data conflict with some concurrent transaction, but also because of limitations on the instructions that can be executed, the time that can be consumed, or the size or associativity of the space used to buffer speculative reads and writes. As programmers begin to write programs with transactions— particularly large transactions—scaling problems will be inevitable, and we can expect growing demand for programming techniques that minimize transaction conflicts and hardware overflow. This abstract introduces one such technique. It exploits the common pattern in which (1) a transaction spends significant time “figuring out what it wants to update” before actually making the updates, and (2) the decision as to what to update can be checked for correctness more easily than it could be computed in the first place. A transaction that satisfies these properties can then be partitioned into a planning phase and an update phase: the planning phase determines what to update; the update phase double-checks the correctness of the plan and performs it. The planning phase can often be performed in ordinary code; the update phase remains a true transaction. Information is passed between the two in the form of a validator object that encapsulates a description of the desired update (the plan) and whatever information is needed to confirm its continued correctness. Partitioned transactions (ParT) complicate the creation and use of concurrent objects, but not dramatically so. Consider a sorted linked list, where each operation begins by scanning the entire sublist from the head to the neighborhood of a given key. In a naive implementation, a transaction will abort if another thread updates any element of the already-scanned sublist. If every element indicates the list to which it belongs, however, then the neighborhood of a key is self evident once found, and we can effect a dramatic reduction in conflicts by separating the scan from the update. Transactional calls to L.insert(x), for example, would be replaced by L.insert plan(&v, x); L.insert update(v). With a bit of care, the planning phase can be performed in ordinary (nontransactional) code. It initializes the validator object v, which contains both x and a pointer to a node in the to-be-modified neighborhood. The update phase (a transaction) subsequently confirms that the pointer still indicates the right place in the right list, and then makes the appropriate changes. In a more computational vein, consider a transaction that sets y = f(a, b, c); x = y, where a, b, c, and x are shared variables, and f is a time-consuming function. We can significantly reduce the time in which the transaction is subject to conflict by computing y = f(a, b, c) nontransactionally; passing a validator containing a, b, c, and y; and making the update transaction the equivalent of an n-word compare and swap. If it is essential to the correctness of f that a, b, and c be consistent, we can put the three reads in a transaction. For common operations, partitioning can lead to dramatically lower abort rates and higher scalability. We have confirmed these