初始化集合时,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)。
与其他集合类型(例如List
, Dictionary
)不同, HashSet
没有使用capacity
参数来设置初始容量的构造函数。 如果要使用大量元素初始化HashSet
,最有效的方法可能是首先将元素添加到具有适当容量的数组或List
,然后传递此数组或列表到HashSet
构造函数。
我猜HashSet
与大多数.net集合一样,使用数组倍增策略进行增长。 不幸的是,没有构造函数重载需要容量。
但是,如果它检查ICollection
并使用ICollection
作为初始容量,您可以实现ICollection
的基本实现,该实现实现GetEnumerator()
和Count
。 这样,您可以直接填充HashSet
而无需实现临时List
。
如果你将1亿个整数放入一个消耗1.5GB的哈希集(我的机器)如果你创建一个bool [100000000],你设置每个数字你必须为真,它只需要100MB,并且查找速度也比哈希集快。 假设整数范围为0-100000000
HashSet
通过加倍增长,并且分配导致它超过可用内存。
在64位系统上,HashSet可以容纳超过8900万个项目 。
在32位系统上,限制大约为6170万个项目 。
这就是你得到内存溢出exception的原因
了解更多信息