Java:当不需要并发时,使用ConcurrentSkipList *的开销是多少?

时间:2011-11-03 02:32:39

标签: java performance data-structures collections concurrency

我需要在迭代主导的场景中排序列表(与插入/删除相比,而不是随机获取)。出于这个原因,我考虑使用跳过列表与树相比(迭代器应该更快)。

问题是java6只有一个跳过列表的并发实现,所以我猜测在非并发场景中使用它是否有意义,或者开销是否是一个错误的决定。

据我所知,ConcurrentSkipList *基本上是基于CAS的无锁实现,所以它们不应该承担(很多)开销,但我想听听别人的意见。

编辑:

经过一些微基准测试(在不同大小的TreeSet,LinkedList,ConcurrentSkipList和ArrayList上多次运行迭代)后,显示出相当大的开销。 ConcurrentSkipList确实将元素存储在链接列表中,因此迭代的速度比LinkedList慢的唯一原因是由于前面提到的开销。

6 个答案:

答案 0 :(得分:3)

如果不需要线程安全,我会说完全跳过package java.util.concurrent

  

有趣的是,有时ConcurrentSkipList在同一输入上比TreeSet慢,但我还没有解决原因。

我的意思是,您是否看过ConcurrentSkipListMap的源代码? :-)当我看着它时,我总是要微笑。它是3000行中最疯狂,最可怕,同时也是我在Java中见过的漂亮代码。 (感谢Doug Lea和co。让所有的并发工具与集合框架很好地集成!)尽管如此,在现代CPU上,代码和算法的复杂性甚至都不会那么重要。通常会产生更大差异的是让数据迭代共存于内存中,以便CPU缓存可以更好地完成工作。

  

所以最后我将使用一个新的addSorted()方法将ArrayList包装起来,该方法将一个已排序的插入到ArrayList中。

听起来不错。如果你真的需要从迭代中挤出每一滴性能,你也可以尝试直接迭代原始数组。每次更改时重新填充,例如通过调用TreeSet.toArray()或生成它,然后使用Arrays.sort(T[], Comparator<? super T>)对其进行排序。但是增益可能很小(如果JIT能很好地完成工作,甚至没有任何收获),因此可能不值得给您带来不便。

答案 1 :(得分:3)

在我公司使用的典型生产硬件上使用Open JDK 6进行测量时,您可以期望在跳过列表映射上的所有添加和查询操作大致采用 double 时间作为相同的操作树图。

的示例:

63 usec vs 31 usec创建并添加200个条目。 对于该200个元素的映射,get()为145 ns vs 77 ns。

对于较小和较大的尺寸,这个比例并没有太大变化。

(此基准测试的代码最终会被共享,因此您可以自行查看并自行运行;抱歉,我们尚未准备好这样做。)

答案 2 :(得分:1)

你可以使用很多其他结构来做跳过列表,它存在于Concurrent包中,因为并发数据结构要复杂得多,并且使用并发跳过列表比使用其他并发数据结构模仿成本要低跳过清单。

在单个线程中,世界是不同的:您可以使用排序集,二叉树或自定义数据结构,其性能优于并发跳过列表。

迭代树列表或跳过列表的复杂性将始终为O(n),但如果您改为使用链接列表或数组列表,则会出现插入问题:将项目插入正确的位置(排序链表)对于二叉树或跳过列表,插入的复杂性将是O(n)而不是O(log n)。

您可以在TreeMap.keySet()中迭代以按顺序获取所有插入的键,并且它不会那么慢。

还有TreeSet类,可能就是你所需要的,但由于它只是TreeMap的包装器,所以直接使用TreeMap会更快。

答案 3 :(得分:1)

如果没有并发性,使用平衡二叉搜索树通常会更有效。在Java中,这将是TreeMap

跳过列表通常保留用于并发编程,因为它们易于在多线程应用程序中实现速度。

答案 4 :(得分:1)

你似乎很好地掌握了这里的权衡,所以我怀疑任何人都可以给你一个明确的,有原则的答案。幸运的是,测试非常简单。

我开始创建一个简单的Iterator<String>,它无限循环遍历随机生成的有限字符串列表。 (即:在初始化时,它会从 c 池中生成 a 长度为 b 的随机字符串的数组_strings第一次调用next()会返回_strings[0],下一个调用会返回_strings[1] ...( n +1)调用返回_strings[0]再次。)这个迭代器返回的字符串是我在SortedSet<String>.add(...)SortedSet<String>remove(...)的所有调用中使用的字符串。

然后我写了一个测试方法,接受空SortedSet<String>并循环 d 次。在每次迭代中,它添加 e 元素,然后删除 f 元素,然后迭代整个集合。 (作为一个完整性检查,它使用add()remove()的返回值来跟踪集合的大小,并且当遍历整个集合时,它确保它找到预期的元素数量大多数情况下我这样做只是为了在循环体中有某些东西。)

我认为我不需要解释我的main(...)方法的作用。 : - )

我尝试了各种参数的各种值,我发现有时ConcurrentSkipListSet<String>表现得更好,有时TreeSet<String>表现得更好,但差异从来不会超过两倍。通常,ConcurrentSkipListSet<String>在以下情况下效果更佳:

  • a b 和/或 c 相对较大。 (我的意思是,在我测试的范围内。我的 a 的范围从1000到10000,我的 b 从3到10,我的 c 从10到80.总的来说,最终的设置大小范围从大约450到10000,模式分别为666和6666,因为我通常使用 e = 2 f < / em>。)这表明ConcurrentSkipListSet<String>使用更大的集合和/或更昂贵的字符串比较,比TreeSet<String>略胜一筹。尝试设计用于区分这两个因素的特定值,我得到的结论是ConcurrentSkipListSet<String>明显优于TreeSet<String>更大的集合,而更少与更昂贵的字符串相比-comparisons。 (这基本上是你所期望的; TreeSet<String>的二叉树方法旨在进行绝对最小可能的比较次数。)
  • e f 很小;也就是说,当我每次迭代调用add(...)remove(...)时,只需要很少次。 (这正是您所预测的。)确切的转换点取决于 a b c ,但首先是近似值当 e + f 小于10时,ConcurrentSkipListSet<String>效果更好, e +时TreeSet<String>表现更好 f 超过20岁。

当然,这是在一台看起来与你无关的机器上,使用的JDK可能看起来与你的不同,并且使用看似与你无关的非常人为的数据。我建议你运行自己的测试。由于Tree*ConcurrentSkipList*都实现了Sorted*,因此您可以毫不费力地尝试代码并查看所找到的内容。

  

据我所知,ConcurrentSkipList *基本上是基于CAS的无锁实现,因此它们不应该带来(很多)开销,[...]

我的理解是这取决于机器。在某些系统上,可能无法实现无锁实现,在这种情况下,这些类必须使用锁。 (但是因为你实际上并不是多线程,所以即使锁定也可能不是那么昂贵。当然,同步有开销,但其成本是锁争用和强制单线程。这对你来说不是问题。再次,我认为你只需要测试并看看这两个版本是如何运行的。)

答案 5 :(得分:0)

如前所述,与TreeMap相比,SkipList有很多开销,而TreeMap迭代器并不适合您的用例,因为它只是反复调用方法successor(),结果非常慢。
因此,一个比前两个快得多的替代方法是编写自己的TreeMap迭代器。实际上,我会完全转储TreeMap,因为3000行代码比你可能需要的要大一些,只需用你需要的方法编写一个干净的AVL树实现。基本的AVL逻辑只是一个few hundred lines of code in any language,然后添加最适合您的迭代器。