初始化集合时,hashset对内存有什么作用?

我偶然发现了以下问题。
我想要一个所有数字从1到100.000.000的哈希集。 我尝试了以下代码:

var mySet = new HashSet(); for (var k = 1; k <= 100000000; k++) mySet.Add(k); 

那个代码没有成功,因为我在49mil附近的内存溢出。 这也很慢,内存增长过度。

然后我尝试了这个。

 var mySet = Enumerable.Range(1, 100000000).ToHashSet(); 

其中ToHashSet()是以下代码:

 public static HashSet ToHashSet(this IEnumerable source) { return new HashSet(source); } 

我再次获得了内存溢出,但是我能够使用之前的代码输入更多数字。

有效的方法如下:

 var tempList = new List(); for (var k = 1; k <= 100000000; k++) tempList.Add(k); var numbers = tempList.ToHashSet(); 

我的系统需要大约800毫秒来填充tempList,其中Enumerable.Range()只需要4个滴答!

我确实需要HashSet,否则它需要花费很多时间来查找值(我需要它是O(1)),如果我能以最快的方式做到这一点会很棒。

现在我的问题是:
为什么前两种方法导致内存溢出,而第三种方法没有?

HashSet在初始化时是否有特殊的内存?

我的系统有16GB的内存,所以当我得到溢出exception时我很惊讶。

与其他集合类型一样,HashSet会在您添加元素时根据需要自动增加其容量。 添加大量元素时,将导致大量重新分配。

如果使用带有IEnumerable的构造函数初始化它,它将检查IEnumerable实际上是否为ICollection ,如果是,则将HashSet的容量初始化为集合的大小。

这就是你的第三个例子 – 你正在添加一个List ,它也是一个ICollection ,所以你的HashSet的初始容量等于列表的大小,从而确保没有重新分配是必要的。

如果使用带有容量参数的List构造函数,则效率会更高,因为这将避免在构建列表时重新分配:

 var noElements = 100000000; var tempList = new List(noElements); for (var k = 1; k <= noElements; k++) tempList.Add(k); var numbers = tempList.ToHashSet(); 

至于你的系统内存; 检查这是32位还是64位进程。 32位进程最多可提供2GB内存(如果使用/ 3GB启动开关,则为3GB)。

与其他集合类型(例如ListDictionary )不同, HashSet没有使用capacity参数来设置初始容量的构造函数。 如果要使用大量元素初始化HashSet ,最有效的方法可能是首先将元素添加到具有适当容量的数组或List ,然后传递此数组或列表到HashSet构造函数。

我猜HashSet与大多数.net集合一样,使用数组倍增策略进行增长。 不幸的是,没有构造函数重载需要容量。

但是,如果它检查ICollection并使用ICollection.Count作为初始容量,您可以实现ICollection的基本实现,该实现实现GetEnumerator()Count 。 这样,您可以直接填充HashSet而无需实现临时List

如果你将1亿个整数放入一个消耗1.5GB的哈希集(我的机器)如果你创建一个bool [100000000],你设置每个数字你必须为真,它只需要100MB,并且查找速度也比哈希集快。 假设整数范围为0-100000000

HashSet通过加倍增长,并且分配导致它超过可用内存。

64位系统上,HashSet可以容纳超过8900万个项目

32位系统上,限制大约为6170万个项目

这就是你得到内存溢出exception的原因

了解更多信息

http://blog.mischel.com/2008/04/09/hashset-limitations/