【问题标题】:Hashset memory overhead哈希集内存开销
【发布时间】:2014-09-11 09:34:04
【问题描述】:

C# 程序中,我有两个Queues longs26M 元素和四个HashSets longs50M 元素在一起。所以我的容器正在存储75Mlongs,这提供了600MB 的数据。程序的内存使用是3GB

为什么这些容器需要这么多内存? HashSet 的内存复杂度是多少?即使所有结构的容量翻倍,它也会提供1.2GB,而不是3GB

编辑: 是的,我不是指复杂性。 HashSet 需要额外存储多少内存 long?简单的二叉堆不需要任何额外的内存。如果我需要降低内存使用量或者我需要自己实现一个,HashSet 有什么替代方案吗?

【问题讨论】:

  • 内存复杂度应该是O(n)。也许你想问一下 HashSet 的存储开销? (抱歉,我不知道这个问题的答案。)
  • "HashSet 有什么替代方案吗" 取决于,你需要集合做什么?您是否需要能够将更多项目添加到集合中?添加的那些项目是否需要检查重复?可能有更轻的选择,但您需要告诉我们要求。
  • @ScottChamberlain 这个程序正在检查状态(多头)以确定是赢还是输。因此,当我处理一种状态时,我会检查连接状态是否有赢或输(使用Contains),然后有时我会将已处理状态添加到赢或输状态。
  • 您如何测量正在使用的内存?仅仅因为任务管理器说您正在使用 3gb 内存并不意味着您的应用程序当前正在使用所有这些内存。根据系统有多少内存以及当前运行的内容,这个数字可能会有很大差异。
  • 您是否正在尝试解决问题?

标签: c# memory hashset


【解决方案1】:

概述

HashSet 每个插槽有 12 个字节的开销(可以包含一个项目或为空)。这个开销比存储 long 的情况下的数据大小大 150%。

HashSet 还为新数据保留空槽,并且示例中的项目数(每个 HashSet 约 1250 万个项目)仅由于空槽而导致内存使用量增加约 66%。

如果您需要 O(1) 确认集合中的存在,那么 HashSet 可能是您能做的最好的。如果您知道您的数据有什么特别之处(例如,它包含连续“运行”数百个项目),那么您可能会想出一种更聪明的方式来表示这种需要更少内存的方式。在不了解更多数据的情况下,很难就此提出建议。

测试程序

    static void Main(string[] args)
    {
        var q = new Queue<long>();
        var hs = new []
        {
            new HashSet<long>(),
            new HashSet<long>(),
            new HashSet<long>(),
            new HashSet<long>()
        };

        for (long i = 0; i < 25000000; ++i)
        {
            q.Enqueue(i);

            if (i < 12500000)
            {
                foreach (var h in hs)
                {
                    h.Add(i);
                }
            }
        }

        Console.WriteLine("Press [enter] to exit");
        Console.ReadLine();
    }

HashSet 实现 - 单声道

插槽分配策略 - 每次分配时将表的大小加倍。

https://github.com/mono/mono/blob/master/mcs/class/System.Core/System.Collections.Generic/HashSet.cs

HashSet 实现 - MSFT

插槽分配策略 - 使用素数进行分配。这可能会导致大量空白空间,但会减少必须重新分配和重新散列表的次数。

http://referencesource.microsoft.com/#System.Core/System/Collections/Generic/HashSet.cs

内存使用 - 一般大小调整 - 单声道实现

  • 初始大小:10 个插槽
  • 填充系数:90% 完全触发调整大小
  • 调整大小因子:达到填充因子时大小翻倍

内存使用 - 每个插槽 - 两种实现

  • 表:每个插槽 1 x 4 字节 int = 4 字节/插槽
  • 链接:每个插槽 2 x 4 字节整数 = 8 字节/插槽
  • 槽:1 x (sizeof T) 字节 = 8 字节/槽(对于 T = long)
  • 总计:20 字节/槽

示例中使用的插槽 - 单声道

该示例在每个 HashSet 中有 1250 万个项目。

slots = 10 * 2 ^ 上限(log2(items / 10))

log2(12,500,000 /10) ~= 20.5

插槽 ~= 2100 万

示例中使用的内存 - 计算 - 单声道

队列:2500 万长 * 8 字节 / 长 = 200 MB

每个 HashSet:2100 万个插槽 * 20 字节/插槽 = 420 MB

所有哈希集:1.68 GB

总计:1.88 GB(+ 大对象堆中的空白空间)

示例中使用的内存 - 使用 Son of Strike 观察 - MSFT 实现

.Net 堆中的 3.5 GB 内存

400 MB 的 Int32 数组(由 HashSet 使用,不用于我们的数据存储)

2.5 GB 的 HashSet Slot 对象

注意:MSFT 的 Slot 对象是 8 个字节加上数据的大小(在本例中为 8 个字节),总共 16 个字节。 2.5 GB 的 Slot 对象是 1.56 亿个 Slot,仅用于存储 5000 万个项目。

dumpheap -stat

!dumpheap -stat
Statistics:
              MT    Count    TotalSize Class Name
00007ffb549af228        1           24 System.Collections.Generic.GenericEqualityComparer`1[[System.Int64, mscorlib]]
[snip]
00007ffb53e80bd8      159         6926 System.String
00007ffb53e81250       27        36360 System.Object[]
00000042ed0a8a30       22     48276686      Free
00007ffb53f066f0        3    402653256 System.Int64[]
00007ffb53e83768       14    431963036 System.Int32[]
00007ffaf5e17e88        5   2591773968 System.Collections.Generic.HashSet`1+Slot[[System.Int64, mscorlib]][]
Total 343 objects

eeheap -gc

!eeheap -gc
Number of GC Heaps: 1
generation 0 starts at 0x00000042800472f8
generation 1 starts at 0x0000004280001018
generation 2 starts at 0x0000004280001000
ephemeral segment allocation context: none
 segment     begin allocated  size
0000004280000000  0000004280001000  000000428004b310  0x4a310(303888)
Large object heap starts at 0x0000004290001000
 segment     begin allocated  size
0000004290000000  0000004290001000  0000004290009728  0x8728(34600)
00000042dc000000  00000042dc001000  00000042e7717e70  0xb716e70(191983216)
000000433e6e0000  000000433e6e1000  000000434f9835b0  0x112a25b0(287974832)
00000043526e0000  00000043526e1000  000000435a6e1038  0x8000038(134217784)
000000435e6e0000  000000435e6e1000  0000004380c25c00  0x22544c00(575949824)
00000043826e0000  00000043826e1000  000000438826c788  0x5b8b788(95991688)
000000438a6e0000  000000438a6e1000  00000043acc25c00  0x22544c00(575949824)
00000043ae6e0000  00000043ae6e1000  00000043b426c788  0x5b8b788(95991688)
00000043b66e0000  00000043b66e1000  00000043d8c25c00  0x22544c00(575949824)
00000043da6e0000  00000043da6e1000  00000043e026c788  0x5b8b788(95991688)
00000043e26e0000  00000043e26e1000  0000004404c25c00  0x22544c00(575949824)
0000004298000000  0000004298001000  00000042a8001038  0x10000038(268435512)
Total Size:              Size: 0xcf1c1560 (3474724192) bytes.
------------------------------
GC Heap Size:            Size: 0xcf1c1560 (3474724192) bytes.

【讨论】:

  • 你链接了单声道的 HashSet 实现,here is the implementation from Microsoft
  • 谢谢 Scott - 没有足够快地找到那个链接,所以我选择了 Mono :) 看起来 MSFT 的实现可能会有更多的空槽,因为它通过选择下一个最大的素数来调整大小。实际上,MSFT Slot 包含两个整数和一个 T,即每个 Slot 16 字节,在此示例中提供了 1.62 亿个插槽,仅用于存储 5000 万个项目(使用实际数据之外 223% 的额外存储空间)。
  • 微软在二月份更新了他们的参考源网站,添加了新的网络界面,很多人还不知道。
  • 遗憾的是,@huntharo 没有指定/链接到他们在撰写此答案时使用的 HashSet&lt;T&gt; 的实际版本,因此提供的数字很可能不再有效。例如,Mono 不再有自己的 HashSet;他们使用参考源中的 Microsoft 版本,因此行为应该相同。
  • 如果您想要那个版本的 Hashset,只需查看我提供的 git 链接的 git 历史记录,并从我提交答案之日起将其拉出​​。但是,如果 Mono 实际上放弃了他们的实现,转而支持 MSFT,那可能真的没那么有趣了。在这种情况下,只需查看其实现的分析(假设您使用的 Mono 版本足够新,以便该更改适用于您)。
猜你喜欢
  • 1970-01-01
  • 2015-03-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-11-16
  • 2011-02-12
相关资源
最近更新 更多