跳过列表与二进制搜索树

时间:2008-11-02 04:39:56

标签: algorithm language-agnostic data-structures binary-tree skip-lists

我最近遇到了称为skip list的数据结构。它似乎与二叉搜索树具有非常相似的行为。

为什么你想在二叉搜索树上使用跳过列表?

7 个答案:

答案 0 :(得分:233)

跳过列表更适合并发访问/修改。 Herb Sutter在并发环境中写了一篇关于数据结构的article。它有更深入的信息。

最常用的二叉搜索树实现是red-black tree。当树被修改时,并发问题经常需要重新平衡。重新平衡操作可能会影响树的大部分,这需要在许多树节点上进行互斥锁定。将节点插入跳过列表更加本地化,​​只需要锁定直接链接到受影响节点的节点。


Jon Harrops的评论更新

我读过弗雷泽和哈里斯的最新论文Concurrent programming without locks。如果您对无锁数据结构感兴趣,那真是好东西。本文重点关注Transactional Memory和理论操作多字比较和交换MCAS。这两个都是在软件中模拟的,因为还没有硬件支持它们。我对他们能够在软件中构建MCAS印象深刻。

我没有发现事务性内存的东西特别引人注目,因为它需要一个垃圾收集器。同样software transactional memory也受到性能问题的困扰。但是,如果硬件事务内存变得普遍,我会非常兴奋。最后,它仍在研究中,并且在未来十年左右不会用于生产代码。

在8.2节中,他们比较了几个并发树实现的性能。我将总结他们的发现。下载pdf是值得的,因为它在第50,53和54页上有一些非常丰富的图表。

  • 锁定跳过列表非常快。它们以并发访问的数量非常好地扩展。这使得跳过列表变得特别,其他基于锁的数据结构在压力下往往会嘎然而止。
  • 无锁跳过列表始终比锁定跳过列表更快,但只是勉强。
  • 事务性跳过列表始终比锁定和非锁定版本慢2-3倍。
  • 在并发访问下锁定红黑树。它们的性能随每个新的并发用户线性降低。在两种已知的锁定红黑树实现中,一种在树重新平衡期间基本上具有全局锁定。另一个使用花哨(和复杂)的锁升级,但仍然没有显着执行全局锁定版本。
  • 无锁红黑树不存在(不再适用,请参阅更新)。
  • 交易红黑树与交易跳过列表相当。这是非常令人惊讶和非常有希望的。事务性内存虽然更慢但更容易编写。它可以像在非并发版本上快速搜索和替换一样简单。

更新
以下是关于无锁树的文章:Lock-Free Red-Black Trees Using CAS 我没有深入研究过,但从表面上看它似乎很坚固。

答案 1 :(得分:72)

首先,您无法将随机数据结构与提供最坏情况保证的数据结构进行公平比较。

跳过列表相当于随机平衡的二叉搜索树(RBST),其方式将在Dean和Jones& Sons等中详细说明。 "Exploring the Duality Between Skip Lists and Binary Search Trees"

反过来说,您还可以使用确定性跳过列表来保证最差情况下的性能,参见Munro et al.

与上面提到的一些内容相反,您可以实现二进制搜索树(BST)在并发编程中运行良好。以并发为重点的BST的一个潜在问题是,您可以轻松获得与红黑(RB)树相同的平衡保证。 (但&#​​34;标准",即随机,跳过列表也不能给你这些保证。)在任何时候保持平衡和良好(易于编程)之间需要权衡利弊。并发访问,因此当需要良好的并发性时,通常会使用宽松 RB树。放松包括不立即重新平衡树。对于一个有点过时(1998年)的调查,请参阅Hanke的“并行红黑树算法的表现”'' [ps.gz]

最新的改进之一就是所谓的彩色树(基本上你有一些重量,黑色为1,红色为零,但你也允许它们之间的值)。一个彩色树如何对抗跳过列表?让我们看看布朗等人的观点。 "A General Technique for Non-blocking Trees"(2014)不得不说:

  

有128个线程,我们的算法优于Java的非阻塞跳转列表   Bronson等人的基于锁定的AVL树由13%到156%。 63%到224%,以及使用软件事务内存(STM)13到134次的RBT

编辑添加:Pugh的基于锁定的跳过列表,在Fraser和Harris(2007)"Concurrent Programming Without Lock"中进行了基准测试,接近他们自己的无锁版本(这一点非常坚持这里的最佳答案),也被调整为良好的并发操作,参见Pugh的"Concurrent Maintenance of Skip Lists",虽然方式相当温和。然而,Herlihy等人提出的一篇较新的2009年论文"A Simple Optimistic skip-list Algorithm",提出了一个比较简单(比Pugh's)基于锁定的并发跳过列表的实现,批评Pugh没有提供足够令人信服的证明对他们来说撇开这个(可能太迂腐)的质疑,Herlihy等人。表明他们更简单的基于锁定的跳过列表实现实际上无法扩展以及JDK的无锁实现,但仅限于高争用(50%插入,50%删除和0%查找)。 ......弗雷泽和哈里斯根本没有考验过。 Fraser和Harris仅测试了75%的查找,12.5%的插入和12.5%的删除(在跳过列表中有~500K元素)。 Herlihy等人的简单实现。在他们测试的低争用情况下,也接近JDK的无锁解决方案(70%查找,20%插入,10%删除);当他们使跳过列表足够大时,他们实际上击败了这种情况下的无锁解决方案,即从200K到2M元素,这样任何锁定的争用概率都可以忽略不计。如果Herlihy等人的话会很好。我已经克服了Pugh的证据,并测试了他的实施,但唉他们没有做到。

EDIT2:我找到了一份(2015年出版的)所有基准的母亲:Gramoli' "More Than You Ever Wanted to Know about Synchronization. Synchrobench, Measuring the Impact of the Synchronization on Concurrent Algorithms":这是一个与这个问题相关的摘录图像。

enter image description here

" Algo.4"是上面提到的Brown等人的前身(旧版,2011版)。 (我不知道2014版本有多好或多差)。 " Algo.26"是上面提到的Herlihy;正如你所看到的那样,它会在更新时被破坏,而在这里使用的英特尔CPU比原始论文中的太阳CPU更糟糕。 " Algo.28"是来自JDK的ConcurrentSkipListMap;与其他基于CAS的跳过列表实现相比,它没有像人们希望的那样好。高争夺的赢家是" Algo.2" Crain等人描述的基于锁的算法(!!)。在"A Contention-Friendly Binary Search Tree"和" Algo.30"是"旋转跳过列表"来自"Logarithmic data structures for multicores"。 " Algo.29"是"No hot spot non-blocking skip list"。请注意,Gramoli是所有这三个获胜者算法论文的共同作者。 " Algo.27"是Fraser的跳过列表的C ++实现。

Gramoli的结论是,搞砸基于CAS的并发树实现要比搞砸类似的跳过列表容易得多。根据这些数字,很难不同意。他对这个事实的解释是:

  

设计无锁树的困难源于   原子地修改多个引用的难度。跳过列表   由通过后继指针相互连接的塔组成   其中每个节点指向紧邻其下方的节点。他们是   通常认为与树类似,因为每个节点都有一个后继节点   然而,在继承塔及其下方,一个主要的区别是   向下指针通常是不可变的,因此简化了   节点的原子修改。这种区别可能是   跳过列表的原因是在竞争激烈的情况下优于树   如图[上面]所示。

克服这一困难是布朗等人近期工作中的一个关键问题。 他们有一份完整的(2013)论文"Pragmatic Primitives for Non-blocking Data Structures" 建立多记录LL / SC复合"原语",他们称之为LLX / SCX,他们自己使用(机器级)CAS实现。布朗等人。在2014年(但不是2011年)的并发树实现中使用了这个LLX / SCX构建块。

我认为这也许值得总结一下这些基本思想 的"no hot spot"/contention-friendly (CF) skip list。它从轻松的RB树(以及相似的concrrency friedly数据结构)中获得了一个重要的想法:塔在插入后不再立即建立,而是推迟到争用较少的时候。相反,删除高塔可能会产生许多争议; 这可以追溯到Pugh 1990年同时的跳过列表文件,这就是为什么Pugh在删除时引入指针反转的原因(维基百科在跳过列表上的页面仍然没有提到这一点一天,唉)。 CF跳过列表更进一步,延迟删除高塔的上层。 CF跳过列表中的两种延迟操作都是由(基于CAS)单独的类似垃圾收集器的线程执行的,其作者称之为“适应线程”#34;。

Synchrobench代码(包括所有经过测试的算法)可在以下网址获得:https://github.com/gramoli/synchrobench。 最新的布朗等人。可以在http://www.cs.toronto.edu/~tabrown/chromatic/ConcurrentChromaticTreeMap.java获得实施(不包括在上面)。是否有人拥有32+核心机器? J / K我的观点是你可以自己运行这些。

答案 2 :(得分:12)

此外,除了给出的答案(易于实施与性能与平衡树相当)。我发现实现有序遍历(前向和后向)要简单得多,因为跳过列表在其实现中有效地有一个链表。

答案 3 :(得分:9)

在实践中,我发现我的项目中的B树性能已经比跳过列表更好。跳过列表似乎更容易理解,但实现B树不是很难。

我所知道的一个优点是,一些聪明的人已经研究出如何实现仅使用原子操作的无锁并发跳过列表。例如,Java 6包含ConcurrentSkipListMap类,如果你疯了,你可以读取它的源代码。

但是编写一个并发的B树变种并不难 - 我已经看到它由其他人完成 - 如果你先发制人地拆分和合并节点“以防万一”当你走下树然后你赢了'我不得不担心死锁,并且只需要一次锁定树的两个级别。同步开销会略高,但B树可能更快。

答案 4 :(得分:8)

来自您引用的Wikipedia文章:

  

Θ(n)操作,迫使我们按升序访问每个节点(例如打印整个列表),提供了对跳过列表的级别结构进行幕后去随机化的机会。最佳方式,将跳过列表带到O(log n)搜索时间。 [...]   跳过清单,我们没有   最近执行[任何此类]Θ(n)操作,没有   提供相同的绝对最坏情况   性能保证更多   传统的平衡树数据   结构,因为它总是如此   可能(尽管很低)   使用硬币翻转的概率)   构建跳过列表会产生一个   结构严重不平衡

编辑:所以这是一个权衡:跳过列表使用更少的内存,冒着可能退化为不平衡树的风险。

答案 5 :(得分:2)

使用列表实现跳过列表。

对于单链接和双链接列表存在锁定解决方案 - 但是没有无锁解决方案直接仅将CAS用于任何O(logn)数据结构。

但是,您可以使用基于CAS的列表来创建跳过列表。

(请注意,使用CAS创建的MCAS允许使用MCAS创建任意数据结构和概念证明红黑树。)

所以,虽然它们很奇怪,但它们非常有用: - )

答案 6 :(得分:-1)

跳过列表确实具有锁定剥离的优势。但是,欠幅时间取决于如何确定新节点的级别。通常这是使用Random()完成的。在56000个单词的字典中,跳过列表比展开树花费更多时间,并且树比哈希表花费更多时间。前两个与哈希表的运行时不匹配。此外,哈希表的数组也可以以并发方式锁定。

当需要引用的位置时,使用跳过列表和类似的有序列表。例如:在申请日期和日期之前查找航班。

内存二进制搜索splay树很棒且使用频率更高。

Skip List Vs Splay Tree Vs Hash Table Runtime on dictionary find op