HashSet与列表性能


404

显然,泛型HashSet<T>类的搜索性能高于泛型List<T>类。只需将基于哈希的键与线性方法进行比较即可List<T>

但是,计算哈希键本身可能会花费一些CPU周期,因此对于少量项,线性搜索可以真正替代HashSet<T>

我的问题:收支平衡在哪里?

为了简化场景(公平地说),我们假定List<T>类使用元素的Equals()方法来标识项目。


7
如果您真的想最大程度地减少查找时间,请考虑使用数组和排序数组。为了正确回答这个问题,需要一个基准测试,但是您需要向我们介绍更多有关T的信息。此外,HashSet的性能可能会受到T.GetHashCode()的运行时间的影响。
Eldritch难题,2012年

Answers:


818

许多人说,一旦达到实际速度,速度就是一个HashSet<T>永远无法克服的问题List<T>,但这取决于您的工作。

假设您的List<T>商品中平均只有5件商品。在大量周期中,如果每个周期添加或删除一个项目,则使用可能会更好List<T>

我在我的机器上对此进行了测试,并且,它必须很小,才能从中获得好处List<T>。对于短字符串列表,对于5号之后的对象,优点在5号之后消失了。

1 item LIST strs time: 617ms
1 item HASHSET strs time: 1332ms

2 item LIST strs time: 781ms
2 item HASHSET strs time: 1354ms

3 item LIST strs time: 950ms
3 item HASHSET strs time: 1405ms

4 item LIST strs time: 1126ms
4 item HASHSET strs time: 1441ms

5 item LIST strs time: 1370ms
5 item HASHSET strs time: 1452ms

6 item LIST strs time: 1481ms
6 item HASHSET strs time: 1418ms

7 item LIST strs time: 1581ms
7 item HASHSET strs time: 1464ms

8 item LIST strs time: 1726ms
8 item HASHSET strs time: 1398ms

9 item LIST strs time: 1901ms
9 item HASHSET strs time: 1433ms

1 item LIST objs time: 614ms
1 item HASHSET objs time: 1993ms

4 item LIST objs time: 837ms
4 item HASHSET objs time: 1914ms

7 item LIST objs time: 1070ms
7 item HASHSET objs time: 1900ms

10 item LIST objs time: 1267ms
10 item HASHSET objs time: 1904ms

13 item LIST objs time: 1494ms
13 item HASHSET objs time: 1893ms

16 item LIST objs time: 1695ms
16 item HASHSET objs time: 1879ms

19 item LIST objs time: 1902ms
19 item HASHSET objs time: 1950ms

22 item LIST objs time: 2136ms
22 item HASHSET objs time: 1893ms

25 item LIST objs time: 2357ms
25 item HASHSET objs time: 1826ms

28 item LIST objs time: 2555ms
28 item HASHSET objs time: 1865ms

31 item LIST objs time: 2755ms
31 item HASHSET objs time: 1963ms

34 item LIST objs time: 3025ms
34 item HASHSET objs time: 1874ms

37 item LIST objs time: 3195ms
37 item HASHSET objs time: 1958ms

40 item LIST objs time: 3401ms
40 item HASHSET objs time: 1855ms

43 item LIST objs time: 3618ms
43 item HASHSET objs time: 1869ms

46 item LIST objs time: 3883ms
46 item HASHSET objs time: 2046ms

49 item LIST objs time: 4218ms
49 item HASHSET objs time: 1873ms

这是显示为图形的数据:

在此处输入图片说明

这是代码:

static void Main(string[] args)
{
    int times = 10000000;


    for (int listSize = 1; listSize < 10; listSize++)
    {
        List<string> list = new List<string>();
        HashSet<string> hashset = new HashSet<string>();

        for (int i = 0; i < listSize; i++)
        {
            list.Add("string" + i.ToString());
            hashset.Add("string" + i.ToString());
        }

        Stopwatch timer = new Stopwatch();
        timer.Start();
        for (int i = 0; i < times; i++)
        {
            list.Remove("string0");
            list.Add("string0");
        }
        timer.Stop();
        Console.WriteLine(listSize.ToString() + " item LIST strs time: " + timer.ElapsedMilliseconds.ToString() + "ms");


        timer = new Stopwatch();
        timer.Start();
        for (int i = 0; i < times; i++)
        {
            hashset.Remove("string0");
            hashset.Add("string0");
        }
        timer.Stop();
        Console.WriteLine(listSize.ToString() + " item HASHSET strs time: " + timer.ElapsedMilliseconds.ToString() + "ms");
        Console.WriteLine();
    }


    for (int listSize = 1; listSize < 50; listSize+=3)
    {
        List<object> list = new List<object>();
        HashSet<object> hashset = new HashSet<object>();

        for (int i = 0; i < listSize; i++)
        {
            list.Add(new object());
            hashset.Add(new object());
        }

        object objToAddRem = list[0];

        Stopwatch timer = new Stopwatch();
        timer.Start();
        for (int i = 0; i < times; i++)
        {
            list.Remove(objToAddRem);
            list.Add(objToAddRem);
        }
        timer.Stop();
        Console.WriteLine(listSize.ToString() + " item LIST objs time: " + timer.ElapsedMilliseconds.ToString() + "ms");



        timer = new Stopwatch();
        timer.Start();
        for (int i = 0; i < times; i++)
        {
            hashset.Remove(objToAddRem);
            hashset.Add(objToAddRem);
        }
        timer.Stop();
        Console.WriteLine(listSize.ToString() + " item HASHSET objs time: " + timer.ElapsedMilliseconds.ToString() + "ms");
        Console.WriteLine();
    }

    Console.ReadLine();
}

8
非常感谢!这是一个很好的解释,我一直在寻找可以比List<T>游戏引擎更快地添加和删除的东西,并且由于我通常会携带大量对象,因此这种收藏非常理想。
redcodefinal 2013年

17
.NET框架中实际上存在一个集合,该集合根据列表中包含的项目数在列表实现和hastable实现之间切换:HybridDictionary
MgSam 2013年

8
MS似乎已经放弃了它的想法,因为它只有一个非通用版本。
MgSam

47
就此答案而言,它无法回答有关列表与哈希集搜索性能的原始问题。您正在测试可以多快地插入和删除它们,这比搜索要花费更多的时间和不同的性能特征。使用.Contains再试一次,您的图形将发生显着变化。
罗伯特·麦基

5
@hypehuman CPU无法直接处理系统内存中的数据,而是将数据从内存中拉入其缓存以进行处理。在要移动的内存请求与实际到达的内存之间存在明显的延迟,因此CPU通常会请求立即移动较大的连续内存块。这背后的想法是,下一条指令所需的内存可能非常接近前一条指令所使用的内存,因此通常已经在高速缓存中。当您的数据分散到整个内存中时,获得幸运的机会就会减少。
罗伊·T。

70

您看错了。是的,对列表的线性搜索将击败HashSet的少量项目。但是性能差异通常对于那么小的集合无关紧要。通常,您需要担心的是大型集合,这就是Big-O的想法。但是,如果您已经衡量了HashSet性能的真正瓶颈,则可以尝试创建混合的List / HashSet,但是您将通过进行大量的经验性能测试来做到这一点-不问SO方面的问题。


5
您必须担心的大量收藏。我们可以用when small collection becomes large enough to worry about HashSet vs List?成千上万的元素重新定义这个问题吗?
om-nom-nom 2012年

8
不,您会在数百个元素上看到相当大的性能差异。如果您正在执行HashSet擅长的访问类型(例如,集合中的元素X),则始终使用HashSet。如果您的集合太小以至于List更快,那么这些查找就很少见了实际上是您应用程序中的瓶颈。如果您可以将其评估为一个,那么可以尝试对其进行优化-否则您会浪费时间。
Eloff 2012年

15
如果您有一个小型收藏被多次打中怎么办?这并非不常见的情况。
dan-gph 2014年

3
@ om-nom-nom-我想说的是,临界点在哪里都没有关系,因为:“如果担心性能,请使用HashSet<T>。在数量较少的情况下,List<T>可能更快,两者之间的差异可忽略不计。”
Scott Smith

66

它本质上是没有意义的两个结构比较的性能不同行为。使用传达意图的结构。即使您说List<T>不会重复,并且迭代顺序也可以使其与a相提并论,但这并不是HashSet<T>一个好选择,List<T>因为它的容错性相对较低。

也就是说,我将检查性能的其他方面

+------------+--------+-------------+-----------+----------+----------+-----------+
| Collection | Random | Containment | Insertion | Addition |  Removal | Memory    |
|            | access |             |           |          |          |           |
+------------+--------+-------------+-----------+----------+----------+-----------+
| List<T>    | O(1)   | O(n)        | O(n)      | O(1)*    | O(n)     | Lesser    |
| HashSet<T> | O(n)   | O(1)        | n/a       | O(1)     | O(1)     | Greater** |
+------------+--------+-------------+-----------+----------+----------+-----------+
  • 即使在这两种情况下加法均为O(1),在HashSet中它的速度都相对较慢,因为在存储哈希码之前会涉及到预先计算哈希码的成本。

  • HashSet出色的可扩展性具有内存成本。每个条目及其哈希码都存储为一个新对象。本文可能会给您一个想法。


11
我的问题(六年前)与理论表现无关。
Michael Damatov 2014年

1
HashSet确实允许使用ElementAt()进行随机访问,我认为那将是O(n)时间。同样,也许您可​​以在表中放置每个集合是否允许重复(例如:列表允许,但哈希集不允许)。
丹·W

1
表中的@DanW我只是在比较性能,而不是行为特征。感谢ElementAt提示。
nawfal

1
ElementAt只是LINQ扩展..它无法做您无法做的事情,并且可以通过添加自己的另一种方法更好地进行优化。我认为该表在不考虑ElementAt的情况下更有意义,因为所有其他方法都明确存在于这些类上。
Dinerdo '18

感谢这张表,在我的用例中,每次启用/禁用目标时,我都需要向填充的集合中添加和删除目标,这有助于我做出正确的选择(HashSet)。
凯西霍夫兰

50

是否使用HashSet <>或List <>取决于您需要如何访问集合。如果您需要保证项目的顺序,请使用列表。如果不这样做,请使用HashSet。让Microsoft担心其哈希算法和对象的实现。

HashSet将访问项目,而不必枚举集合(O(1)或其附近的复杂性),并且由于List保证顺序,与HashSet不同,某些项目将必须枚举(O(n)的复杂性)。


列表可能会通过特定元素的索引来计算其偏移量(因为所有元素都是同一类型并且可能占用相同的内存大小)。因此,List不必枚举它的元素
Lu55,2014年

@ Lu55-问题是关于集合中搜索项目。典型的情况是集合是动态的 -自上次查找给定项目以来,可能已添加或删除了项目-因此索引没有意义(因为它会更改)。如果您有一个静态集合(在执行计算时不会更改),或者从不删除项,并且总是在最后添加项,则List首选a,因为您可以记住索引-就是这种情况正在描述。
制造商史蒂夫(Steve)'18年

如果需要对HashSet进行排序,则可以使用SortedSet。仍然比列表快得多。
live-love

25

只是想我会参考一些针对不同情况的基准来说明先前的答案:

  1. 几个(12-20)小字符串(长度在5到10个字符之间)
  2. 许多(〜10K)小弦
  3. 一些长字符串(长度在200到1000个字符之间)
  4. 许多(〜5K)长字符串
  5. 几个整数
  6. 许多(〜10K)整数

对于每种情况,查找出现的值:

  1. 在列表的开头(“开始”,索引0)
  2. 在列表开头附近(“早期”,索引1)
  3. 在列表的中间(“中间”,索引计数/ 2)
  4. 列表末尾附近(“ late”,索引计数2)
  5. 在列表的末尾(“ end”,索引计数为1)

在每种情况下,我生成随机大小的随机字符串列表,然后将每个列表馈入哈希集。每个方案都运行了10,000次,基本上是:

(测试伪代码)

stopwatch.start
for X times
    exists = list.Contains(lookup);
stopwatch.stop

stopwatch.start
for X times
    exists = hashset.Contains(lookup);
stopwatch.stop

样本输出

在Windows 7、12GB Ram,64位,Xeon 2.8GHz上进行了测试

---------- Testing few small strings ------------
Sample items: (16 total)
vgnwaloqf diwfpxbv tdcdc grfch icsjwk
...

Benchmarks:
1: hashset: late -- 100.00 % -- [Elapsed: 0.0018398 sec]
2: hashset: middle -- 104.19 % -- [Elapsed: 0.0019169 sec]
3: hashset: end -- 108.21 % -- [Elapsed: 0.0019908 sec]
4: list: early -- 144.62 % -- [Elapsed: 0.0026607 sec]
5: hashset: start -- 174.32 % -- [Elapsed: 0.0032071 sec]
6: list: middle -- 187.72 % -- [Elapsed: 0.0034536 sec]
7: list: late -- 192.66 % -- [Elapsed: 0.0035446 sec]
8: list: end -- 215.42 % -- [Elapsed: 0.0039633 sec]
9: hashset: early -- 217.95 % -- [Elapsed: 0.0040098 sec]
10: list: start -- 576.55 % -- [Elapsed: 0.0106073 sec]


---------- Testing many small strings ------------
Sample items: (10346 total)
dmnowa yshtrxorj vthjk okrxegip vwpoltck
...

Benchmarks:
1: hashset: end -- 100.00 % -- [Elapsed: 0.0017443 sec]
2: hashset: late -- 102.91 % -- [Elapsed: 0.0017951 sec]
3: hashset: middle -- 106.23 % -- [Elapsed: 0.0018529 sec]
4: list: early -- 107.49 % -- [Elapsed: 0.0018749 sec]
5: list: start -- 126.23 % -- [Elapsed: 0.0022018 sec]
6: hashset: early -- 134.11 % -- [Elapsed: 0.0023393 sec]
7: hashset: start -- 372.09 % -- [Elapsed: 0.0064903 sec]
8: list: middle -- 48,593.79 % -- [Elapsed: 0.8476214 sec]
9: list: end -- 99,020.73 % -- [Elapsed: 1.7272186 sec]
10: list: late -- 99,089.36 % -- [Elapsed: 1.7284155 sec]


---------- Testing few long strings ------------
Sample items: (19 total)
hidfymjyjtffcjmlcaoivbylakmqgoiowbgxpyhnrreodxyleehkhsofjqenyrrtlphbcnvdrbqdvji...
...

Benchmarks:
1: list: early -- 100.00 % -- [Elapsed: 0.0018266 sec]
2: list: start -- 115.76 % -- [Elapsed: 0.0021144 sec]
3: list: middle -- 143.44 % -- [Elapsed: 0.0026201 sec]
4: list: late -- 190.05 % -- [Elapsed: 0.0034715 sec]
5: list: end -- 193.78 % -- [Elapsed: 0.0035395 sec]
6: hashset: early -- 215.00 % -- [Elapsed: 0.0039271 sec]
7: hashset: end -- 248.47 % -- [Elapsed: 0.0045386 sec]
8: hashset: start -- 298.04 % -- [Elapsed: 0.005444 sec]
9: hashset: middle -- 325.63 % -- [Elapsed: 0.005948 sec]
10: hashset: late -- 431.62 % -- [Elapsed: 0.0078839 sec]


---------- Testing many long strings ------------
Sample items: (5000 total)
yrpjccgxjbketcpmnvyqvghhlnjblhgimybdygumtijtrwaromwrajlsjhxoselbucqualmhbmwnvnpnm
...

Benchmarks:
1: list: early -- 100.00 % -- [Elapsed: 0.0016211 sec]
2: list: start -- 132.73 % -- [Elapsed: 0.0021517 sec]
3: hashset: start -- 231.26 % -- [Elapsed: 0.003749 sec]
4: hashset: end -- 368.74 % -- [Elapsed: 0.0059776 sec]
5: hashset: middle -- 385.50 % -- [Elapsed: 0.0062493 sec]
6: hashset: late -- 406.23 % -- [Elapsed: 0.0065854 sec]
7: hashset: early -- 421.34 % -- [Elapsed: 0.0068304 sec]
8: list: middle -- 18,619.12 % -- [Elapsed: 0.3018345 sec]
9: list: end -- 40,942.82 % -- [Elapsed: 0.663724 sec]
10: list: late -- 41,188.19 % -- [Elapsed: 0.6677017 sec]


---------- Testing few ints ------------
Sample items: (16 total)
7266092 60668895 159021363 216428460 28007724
...

Benchmarks:
1: hashset: early -- 100.00 % -- [Elapsed: 0.0016211 sec]
2: hashset: end -- 100.45 % -- [Elapsed: 0.0016284 sec]
3: list: early -- 101.83 % -- [Elapsed: 0.0016507 sec]
4: hashset: late -- 108.95 % -- [Elapsed: 0.0017662 sec]
5: hashset: middle -- 112.29 % -- [Elapsed: 0.0018204 sec]
6: hashset: start -- 120.33 % -- [Elapsed: 0.0019506 sec]
7: list: late -- 134.45 % -- [Elapsed: 0.0021795 sec]
8: list: start -- 136.43 % -- [Elapsed: 0.0022117 sec]
9: list: end -- 169.77 % -- [Elapsed: 0.0027522 sec]
10: list: middle -- 237.94 % -- [Elapsed: 0.0038573 sec]


---------- Testing many ints ------------
Sample items: (10357 total)
370826556 569127161 101235820 792075135 270823009
...

Benchmarks:
1: list: early -- 100.00 % -- [Elapsed: 0.0015132 sec]
2: hashset: end -- 101.79 % -- [Elapsed: 0.0015403 sec]
3: hashset: early -- 102.08 % -- [Elapsed: 0.0015446 sec]
4: hashset: middle -- 103.21 % -- [Elapsed: 0.0015618 sec]
5: hashset: late -- 104.26 % -- [Elapsed: 0.0015776 sec]
6: list: start -- 126.78 % -- [Elapsed: 0.0019184 sec]
7: hashset: start -- 130.91 % -- [Elapsed: 0.0019809 sec]
8: list: middle -- 16,497.89 % -- [Elapsed: 0.2496461 sec]
9: list: end -- 32,715.52 % -- [Elapsed: 0.4950512 sec]
10: list: late -- 33,698.87 % -- [Elapsed: 0.5099313 sec]

7
有趣。感谢您运行此程序。可悲的是,我怀疑这些讨论会引发不必要的重构。希望对大多数人来说,最重要的是,在绝对最坏的情况下,List仅需0.17 毫秒即可执行一次查找,并且HashSet在查找频率达到荒谬的水平之前,不太可能需要替换它。届时,通常使用List的问题最少。
保罗·沃尔斯

目前这还不是实际的信息。或者也许它本来是错误的...我只是检查了2到8个字符的较小值。将为每个10个值创建列表/哈希集...哈希集的速度要慢30%...如果使用列表中的容量,则相差甚至40%。仅当List没有指定容量并且在添加整个列表之前检查每个值时,HashSet才会提高10%的速度。
Maxim

如果项目数减少到4,则即使在最坏的情况下(相差10%),列表也会再次获胜。因此,我不建议将HashSet用于较小的字符串集合(假设<20)。这与您的“少量测试”不同。
Maxim

1
@Maxim不能真的说我的结果是“错误的”,这就是我的机器上发生的事情。YMMV。实际上,我只是在新的Win10 4.0GHz 16GB固态计算机上再次运行了它们(gist.github.com/zaus/014ac9b5a78b267aa1643d63d30c7554),并获得了相似的结果。我看到的要点是,无论搜索键在何处或列表有多大,哈希集性能都更加一致,而列表性能则从较好的慢到300倍慢地变化。但是正如PaulWalls最初所说,我们正在谈论严重的#microoptimization。
drzaus17年

@Maxim供参考:dotnetfiddle.net/5taRDd-随意使用它。
drzaus17年

10

收支平衡将取决于计算哈希的成本。哈希计算可以很简单,也可以不是……:-)总是有System.Collections.Specialized.HybridDictionary类可以帮助您不必担心盈亏平衡点。


1
您还需要考虑进行比较的成本。对于Contains(T),HashSet将进行比较以检查它是否没有Hash冲突,列表将在找到正确的项之前对其所查看的每个项进行比较。您还必须考虑到T.GetHashCode()生成的哈希值的分布,就好像它总是返回与基本使HashSet与List进行相同操作的值相同。
马丁·布朗

6

与往常一样,答案是“ 取决于 ”。我以您正在谈论的C#标签为基础。

最好的选择是确定

  1. 一组数据
  2. 使用要求

并编写一些测试用例。

它还取决于您对列表进行排序的方式(如果对列表进行了排序),需要进行哪种比较,对列表中的特定对象执行“比较”操作需要多长时间,甚至取决于您打算如何使用列表。采集。

通常,最佳选择不是基于您正在使用的数据大小,而是您打算如何访问它。您是否具有与特定字符串或其他数据相关联的每条数据?基于散列的集合可能是最好的。您存储的数据顺序是否很重要,还是需要同时访问所有数据?定期列出可能会更好。

额外:

当然,我的上述评论假设“性能”意味着数据访问。还有其他需要考虑的问题:当您说“表现”时,您正在寻找什么?表现个人价值在找吗?它是大型(10000、100000或更多)值集的管理吗?它是用数据填充数据结构的性能吗?删除数据?访问单个数据位?替换值?遍历值?内存使用情况?数据复制速度?例如,如果您通过字符串值访问数据,但是您的主要性能要求是最小的内存使用量,那么您可能会遇到冲突的设计问题。


5

您可以使用HybridDictionary,它可以自动检测断点并接受空值,使其本质上与HashSet相同。


1
支持这个想法,但是今天没有人请使用它。对非泛型说不。此外,字典是键-值映射关系,而集合不是。
nawfal 2014年

4

这取决于。如果确切的答案确实很重要,请进行分析并找出答案。如果您确定集合中的元素数量绝对不会超过一定数量,请使用列表。如果数字不受限制,请使用HashSet。


3

取决于您要散列的内容。如果您的键是整数,则在HashSet更快之前,您可能不需要很多项目。如果将其键入字符串,则速度会变慢,并取决于输入字符串。

当然,您可以轻松地提高基准吗?


3

您未考虑的一个因素是GetHashcode()函数的健壮性。有了完善的哈希函数,HashSet显然将具有更好的搜索性能。但是随着哈希函数的减少,HashSet的搜索时间也会减少。


0

取决于许多因素...列表实现,CPU体系结构,JVM,循环语义,equals方法的复杂性等...到列表变得足够大以有效进行基准测试(1000多个元素)时,基于哈希的二进制文件查找胜过线性搜索,而差异仅从那里逐渐扩大。

希望这可以帮助!


1
JVM ...或CLR :-)
bvgheluwe
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.