我记得大约在半年前,有个朋友问我一个问题,现在有一个选型:

一个性能敏感场景,有一个集合,需要确定某一个元素在不在这个集合中,我是用数组直接Contains还是使用HashSet<T>.Contains

大家肯定想都不用想,都选使用HashSet<T>,毕竟HashSet<T>的时间复杂度是O(1),但是后面又附加了一个条件:

这个集合的元素很少,就4-5个。

那这时候就有一些动摇了,只有4-5个元素,是不是用数组Contains或者直接遍历会不会更快一些?当时我也觉得可能元素很少,用数组就够了。

而最近在编写代码时,又遇到了同样的场景,我决定来做一下实验,看看元素很少的情况下,是不是使用数组优于HashSet<T>

测试

我构建了一个测试,分别尝试在不同的容量下,查找一个元素,使用数组和HashSet的区别,代码如下所示:

[GcForce(true)]
[MemoryDiagnoser]
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
public class BenchHashSet
{
private HashSet<string> _hashSet;
private string[] _strings; [Params(1,2,4,64,512,1024)]
public int Size { get; set; } [GlobalSetup]
public void Setup()
{
_strings = Enumerable.Range(0, Size).Select(s => s.ToString()).ToArray();
_hashSet = new HashSet<string>(_strings);
} [Benchmark(Baseline = true)]
public bool EnumerableContains() => _strings.Contains("8192"); [Benchmark]
public bool HashSetContains() => _hashSet.Contains("8192");
}

大家猜猜结果怎么样,就算Size只为1,那么HashSet也比数组Contains遍历快40%。

那么故事就这么结束了吗?所以无论如何场景我们都直接无脑使用HashSet就行了吗?大家看滑动条就知道,故事没有这么简单。

刚刚我们是引用类型的比较,那值类型怎么样?结论就是一样的结果,就算只有1个元素也比数组的Contains快。

那么问题出在哪里?点进去看一下数组Contains方法的实现就清楚了,这个东西使用的是Enumerable迭代器匹配。

那么我们直接来个原始的,Array.IndexOf匹配和for循环匹配试试,于是有了如下代码:

[GcForce(true)]
[MemoryDiagnoser]
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
public class BenchHashSetValueType
{
private HashSet<int> _hashSet;
private int[] _arrays; [Params(1,4,16,32,64)]
public int Size { get; set; } [GlobalSetup]
public void Setup()
{
_arrays = Enumerable.Range(0, Size).ToArray();
_hashSet = new HashSet<int>(_arrays);
} [Benchmark(Baseline = true)]
public bool EnumerableContains() => _arrays.Contains(42); [Benchmark]
public bool ArrayContains() => Array.IndexOf(_arrays,42) > -1; [Benchmark]
public bool ForContains()
{
for (int i = 0; i < _arrays.Length; i++)
{
if (_arrays[i] == 42) return true;
} return false;
} [Benchmark]
public bool HashSetContains() => _hashSet.Contains(42);
}

接下来结果就和我们预想的差不多了,在数组元素小的时候,使用原始的for循环比较会快,然后HashSet就变为最快的了,在更多元素的场景中Array.IndexOf会比for更快:

至于为什么在元素多的情况Array.IndexOf会比for更快,那是因为Array.IndexOf底层使用了SIMD来优化,在之前的文章中,我们多次提到了SIMD,这里就不赘述了。

既然如此我们再来确认一下,到底多少个元素以内用for会更快,可以看到16个元素以内,for循环会快于HashSet:

总结

所以我们应该选择HashSet<T>还是数组呢?这个就需要分情况简单的总结一下:

  • 在小于16个元素场景,使用for循环匹配会比较快。
  • 16-32个元素的场景,速度最快是HashSet<T>然后是Array.IndexOfforIEnumerable.Contains
  • 大于32个元素的场景,速度最快是HashSet<T>然后是Array.IndexOfIEnumerable.Containsfor

从这个上面来看,大于32个元素就不合适直接用for比较了。不过这些差别都很小,除非是性能非常敏感的场景,可以忽略不计,本文解决了笔者的一些困扰,简单记录一下。

数组还是HashSet?的更多相关文章

  1. 2. 三数之和(数组、hashset)

    思路及算法: 该题与第一题的"两数之和"相似,三数之和为0,不就是两数之和为第三个数的相反数吗?因为不能重复,所以,首先进行了一遍排序:其次,在枚举的时候判断了本次的第三个数的值是 ...

  2. C# 数组、HashSet等内存耗尽的解决办法

    在C#中,如果数据量太大,就会出现 'System.OutOfMemoryException' 异常. 解决办法来自于Stack Overflow和MSDN    https://docs.micro ...

  3. 2.请介绍一下List和ArrayList的区别,ArrayList和HashSet区别

    第一问: List是接口,ArrayList实现了List接口. 第二问: ArrayList实现了List接口,HashSet实现了Set接口,List和Set都是继承Collection接口. A ...

  4. HashSet非常的消耗空间,TreeSet因为有排序功能,因此资源消耗非常的高,我们应该尽量少使用

    注:HashMap底层也是用数组,HashSet底层实际上也是HashMap,HashSet类中有HashMap属性(我们如何在API中查属性).HashSet实际上为(key.null)类型的Has ...

  5. 5.秋招复习简单整理之请介绍一下List和ArrayList的区别,arrayList和HashSet区别?

    第一问:List是接口,ArrayList是List的实现类. 第二问:ArrayList是List的实现类,HashSet是Set的实现类,List和Set都实现了Collection接口. Arr ...

  6. JAVA的面向对象编程--------课堂笔记

    面向对象主要针对面向过程. 面向过程的基本单元是函数.   什么是对象:EVERYTHING IS OBJECT(万物皆对象)   所有的事物都有两个方面: 有什么(属性):用来描述对象. 能够做什么 ...

  7. Java琐碎知识点

    jps命令是JDK1.5提供的一条显示当前用户的所有java进程pid的指令,类似Linux上的ps命令简化版,Windows和linux/unix平台都可以用比较常用的参数:-q:只显示pid,不显 ...

  8. linkin大话数据结构--Map

    Map 映射关系,也有人称为字典,Map集合里存在两组值,一组是key,一组是value.Map里的key不允许重复.通过key总能找到唯一的value与之对应.Map里的key集存储方式和对应的Se ...

  9. java库中的具体的集合

    1.ArrayList  一种可以动态增长和缩减的索引序列:速度较慢适合用于不修改太多的元素    采用的数组 2.LinkEdList  一种可以在任何位置进行高效的插入和删除操作的有序序列,适合于 ...

随机推荐

  1. Queue-jumpers - 平衡树

    题面 Ponyo and Garfield are waiting outside the box-office for their favorite movie. Because queuing i ...

  2. 【java】学习路径29-异常捕捉实例

    import java.util.ArrayList;public class ExceptionCatchDemo { public static void main(String[] args) ...

  3. k8s中ingress,service,depoyment,pod如何关联

    k8s中pod通过label标签名称来识别关联,它们的label  name一定是一样的.ingress,service,depoyment通过selector 中app:name来关联 1.查询发布 ...

  4. MQ系列5:RocketMQ消息的发送模式

    MQ系列1:消息中间件执行原理 MQ系列2:消息中间件的技术选型 MQ系列3:RocketMQ 架构分析 MQ系列4:NameServer 原理解析 在之前的篇章中,我们学习了RocketMQ的原理, ...

  5. 第七十一篇:Vue组件的私有和全局注册

    好家伙, 1.组件的父子关系 我们封装三个组件,分别为left组件,right组件和App组件 在封装时: 在封装时,彼此的关系是独立的,并不存在父子关系 在使用时: 在使用时,根据彼此的嵌套关系,形 ...

  6. python3之35个关键字详解

    一. python35个关键字列举: help("keywords")查看运行结果: 1. and.or.not 2. if.elif.else3. for.while4. Tru ...

  7. KingbaseES 普通表在线改为分区表案例

    对大表进行分区,但避免长时间锁表 假设您有一个应用程序,该应用程序具有一个巨大的表,并且需要始终可用.它变得如此之大,以至于在不对其进行分区的情况下对其进行管理变得越来越困难.但是,您又不能使表脱机以 ...

  8. IO流的文件输入输出效率问题

    IO流的文件输入输出效率问题 第一种方法 我用使用一个数组,把FIleInputStream获取到的二进制数存入这个数组,然后使用FIleOutputStream进行输出 缺点:速度慢 优点:不消耗内 ...

  9. LVGL 入门使用教程

    一.准备资料 开发板:ESP32-S3 开发环境:VS Code + PlatformIO 串口屏驱动 TFT-eSPI:https://github.com/Bodmer/TFT_eSPI 触摸驱动 ...

  10. ProxySQL 全局变量详解

    转载自:https://www.jianshu.com/p/b9d2a09d80e2 全局变量概述 ProxySQL的行为可以通过全局变量来调整.有两种配置方式: 在runtime下,使用admin结 ...