【问题标题】:What does a hashset do with memory when initializing a collection?初始化集合时,哈希集对内存有什么作用?
【发布时间】:2012-07-18 09:56:33
【问题描述】:

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

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

该代码没有成功,因为我在 4900 万左右发生了内存溢出。这也很慢,内存增长过快。

然后我尝试了这个。

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

其中 ToHashSet() 为以下代码:

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

我再次遇到内存溢出,但我能够输入更多的数字然后使用之前的代码。

起作用的事情如下:

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

var numbers = tempList.ToHashSet();

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

我确实需要那个 HashSet,否则查找值需要很长时间(我需要它是 O(1)),如果我能以最快的方式做到这一点,那就太好了。

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

HashSet 在初始化时对内存有什么特殊作用吗?

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

【问题讨论】:

  • 需要注意的一点是Enumerable.Range 非常快,因为它在运行时实际上并没有生成任何东西。只有在使用它时(即在ToHashSet 调用中)它才真正开始生成数字。
  • @Chris 不知道。谢谢:)。
  • 它与所有 linq 类型可枚举的东西都是一样的。如果您对枚举或 Select 或其他任何基本上返回更多 ienumerables 的东西执行了 Where ,它将推迟它们的执行,直到它们被使用。知道这一点很有用,因为您可能会因为这种行为而遇到一些问题(尽管我想不出一个简洁的例子)。

标签: c# performance memory collections hashset


【解决方案1】:

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

如果您使用带有IEnumerable&lt;T&gt; 的构造函数对其进行初始化,它将检查IEnumerable&lt;T&gt; 是否实际上是ICollection&lt;T&gt;,如果是,则将HashSet 的容量初始化为集合的大小。

这就是你第三个例子中发生的事情——你添加了一个List&lt;T&gt;,它也是一个ICollection&lt;T&gt;,所以你的HashSet的初始容量等于列表的大小,从而确保没有需要重新分配。

如果您使用带有容量参数的List&lt;T&gt; 构造函数,您将更加高效,因为这将避免在构建列表时重新分配:

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

var numbers = tempList.ToHashSet(); 

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

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

【讨论】:

  • 那么,当 HashSet 重新分配内存时,它实际上是在抛弃旧内存并使用一个完全新的集合,从而使旧内存在 GC 或其他东西之前一直悬而未决吗?否则我可以理解为什么这会更快,但不能理解为什么它会防止内存不足异常......
  • @Chris,确切地说,旧内存在被丢弃时有资格进行 GC,但可能还没有开始 GC。
  • 应用程序是 x64 应用程序。我现在明白为什么首先设置容量确实更有效。我不知道 ICollection 的行为是这样的!非常感谢
  • HashSet 现在有一个初始容量参数。看起来它是在 .NET 4.7.2 中引入的(在提出这个问题大约 4 年后)stackoverflow.com/a/6771986/10728554docs.microsoft.com/en-us/dotnet/api/…
【解决方案2】:

我猜HashSet&lt;T&gt; 和大多数 .net 集合一样,使用数组加倍策略来实现增长。不幸的是,没有占用容量的构造函数重载。

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

【讨论】:

    【解决方案3】:

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

    【讨论】:

    • HashSet 的查找速度是 O(1),bool 数组怎么能比这更快?
    • 直接数组查找也是 O(1),但计算哈希并从存储桶中获取数据比查找数组中的条目更昂贵。并且使用 15 倍以上的内存(可能是因为 hashset 将所有 int 包装到对象中)也不是可以忽略不计的区别..
    • 感谢您的详细说明。如果我要实现它,我将不得不更改我的代码,但我一定会尝试。感谢您的建议。
    【解决方案4】:

    HashSet 翻倍增长,分配导致它超出可用内存。

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

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

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

    更多信息

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

    【讨论】:

    • 那不是真的。我实际上确实有一个包含 1 亿个项目的 HashSet。这就是在 x64 平台/应用程序上。
    • 你能澄清一下你的意思吗? OP 的最终解决方案似乎是放入 1 亿个项目。上面的数字是在谈论加倍策略会在多长时间内遇到内存限制?
    • 啊,对不起,我误解了你的回答。在循环中添加项目确实如此。 (因此触发加倍)
    猜你喜欢
    • 1970-01-01
    • 2018-10-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多