【问题标题】:C# dictionary for large number of items用于大量项目的 C# 字典
【发布时间】:2019-02-06 10:40:43
【问题描述】:

我想了解在 C# 中在内存中存储大量项目的成本。我需要使用的数据结构是字典或类似的。假设我想要拥有的项目数量约为 1 亿,但应用程序不会立即达到该数量。我们需要很长时间才能达到极限。

我担心已摊销的运营成本,但在任何特定时刻我都无法承受太高的成本。所以通常使用动态数据结构,当结构已满时,它会重新分配自己。如果是字典,我认为它甚至会重新索引每个项目。因此,假设我们是应用程序维护 2000 万个刚刚达到字典容量的项目的点。然后,当分配新的字典存储时,需要重新索引这 2000 万个项目。

这就是为什么我认为字典数组可能是个好主意。假设我创建了 256 个字典。这立即将每个内部字典的大小限制为少于 100 万个项目,这应该是可以管理的,以动态建立所有索引,直到 100 万个项目发生。这样做的代价似乎只是每次操作的一个额外索引,以找到要查找的正确字典。

这是一个合理的方法吗?我的分析是正确的还是我认为出于某种原因 C# 字典会表现得更好?还有其他更好的解决方案吗?我正在寻找与 C# 字典具有相同时间复杂度的数据结构。

编辑:字典键是一个随机值,所以我只需咬第一口就可以非常便宜地找到我在 256 个字典数组中的索引。

我目前不考虑使用数据库,因为我希望所有项目都能立即可用,而且成本很低。我确实需要以很少的开销在恒定时间内查找。我可以负担得起插入速度较慢,但​​仍然是恒定的时间。与删除相同,可能会慢一点,但需要恒定的时间。

应该可以将所有项目都放入内存中。这些项目很小,每个大约 50 字节的数据。所以数据结构不能对每个项目有太多的开销。

【问题讨论】:

  • 根据文档“如果 Count 小于容量,则此方法接近 O(1) 操作。如果必须增加容量以容纳新元素,则此方法变为 O(n ) 操作,其中 n 是计数。” docs.microsoft.com/en-us/dotnet/api/…
  • 这取决于您需要对字典中的数据执行哪些操作...您能否举例说明您需要执行的一项操作?
  • 只是一个愚蠢的建议,但是;你为什么不把它放在一个数据库中,让那个数据库为你处理这类问题呢?
  • 假设您创建了 256 个字典的数组,您如何知道哪个键存在于哪个字典中。您不认为在 256 个字典的数组中搜索特定键是一种开销吗?
  • 如果您知道您的内存需求(即您希望字典中的项目数),您可以使用new Dictionary<Tx,Ty>(100000000)“预先”提供容量,并避免您描述的重新存储桶问题。

标签: c# dictionary


【解决方案1】:

更新:自发布以来我已对其进行了编辑:

  • 存储一个固定大小的对象(每次传递的字节[50],
  • 在添加到字典之前预先分配所有这些(而不是在循环中创建对象)
  • 在预分配内容后运行 GC.Collect()。
  • gcAllowVeryLargeObjects 设置为 true。
  • 肯定是为 x64 设置的(以前是这样,但后来我切换到“发布”以在 VS 之外构建和运行......它重置了,哎呀。)
  • 尝试了预先分配和不预先分配字典大小。

现在是代码:

var arrays = new byte[100000000][];
System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch();
stopwatch.Start();
for (var i = 0; i<100000000; i++)
{
    arrays[i] = new byte[50];
}
stopwatch.Stop();
Console.WriteLine($"initially allocating arrays took {stopwatch.ElapsedMilliseconds} ms");
stopwatch.Restart();

GC.Collect();
Console.WriteLine($"GC after array allocation took {stopwatch.ElapsedMilliseconds} ms");

Dictionary<int, byte[]> dict = new Dictionary<int, byte[]>(100000000);
//Dictionary<int, byte[]> dict = new Dictionary<int, byte[]>();

for (var c = 0; c < 100; c++)
{
    stopwatch.Restart();
    for (var i = 0; i < 1000000; i++)
    {
        var thing = new AThing();
        dict.Add((c * 1000000) + i, arrays[(c*100000)+i]);
    }
    stopwatch.Stop();
    Console.WriteLine($"pass number {c} took {stopwatch.ElapsedMilliseconds} milliseconds");
}

Console.ReadLine();

这是我不预先分配字典大小时的输出:

initially allocating arrays took 14609 ms
GC after array allocation took 3713 ms
pass number 0 took 63 milliseconds
pass number 1 took 51 milliseconds
pass number 2 took 78 milliseconds
pass number 3 took 28 milliseconds
pass number 4 took 32 milliseconds
pass number 5 took 133 milliseconds
pass number 6 took 41 milliseconds
pass number 7 took 31 milliseconds
pass number 8 took 27 milliseconds
pass number 9 took 26 milliseconds
pass number 10 took 45 milliseconds
pass number 11 took 335 milliseconds
pass number 12 took 34 milliseconds
pass number 13 took 35 milliseconds
pass number 14 took 71 milliseconds
pass number 15 took 66 milliseconds
pass number 16 took 64 milliseconds
pass number 17 took 58 milliseconds
pass number 18 took 71 milliseconds
pass number 19 took 65 milliseconds
pass number 20 took 68 milliseconds
pass number 21 took 67 milliseconds
pass number 22 took 83 milliseconds
pass number 23 took 11986 milliseconds
pass number 24 took 7948 milliseconds
pass number 25 took 38 milliseconds
pass number 26 took 36 milliseconds
pass number 27 took 27 milliseconds
pass number 28 took 31 milliseconds
..SNIP lots between 30-40ms...
pass number 44 took 34 milliseconds
pass number 45 took 34 milliseconds
pass number 46 took 33 milliseconds
pass number 47 took 2630 milliseconds
pass number 48 took 12255 milliseconds
pass number 49 took 33 milliseconds
...SNIP a load of lines which are all between 30 to 50ms...
pass number 93 took 39 milliseconds
pass number 94 took 43 milliseconds
pass number 95 took 7056 milliseconds
pass number 96 took 33323 milliseconds
pass number 97 took 228 milliseconds
pass number 98 took 70 milliseconds
pass number 99 took 84 milliseconds

您可以清楚地看到它必须重新分配的点。我只是通过将列表的大小加倍并复制当前列表项来猜测,因为最后有很长一段时间它没有这样做。其中一些非常昂贵(30 多秒!哎哟)

如果我预先分配了字典大小,这是输出:

initially allocating arrays took 15494 ms
GC after array allocation took 2622 ms
pass number 0 took 9585 milliseconds
pass number 1 took 107 milliseconds
pass number 2 took 91 milliseconds
pass number 3 took 145 milliseconds
pass number 4 took 83 milliseconds
pass number 5 took 118 milliseconds
pass number 6 took 133 milliseconds
pass number 7 took 126 milliseconds
pass number 8 took 65 milliseconds
pass number 9 took 52 milliseconds
pass number 10 took 42 milliseconds
pass number 11 took 34 milliseconds
pass number 12 took 45 milliseconds
pass number 13 took 48 milliseconds
pass number 14 took 46 milliseconds
..SNIP lots between 30-80ms...
pass number 45 took 80 milliseconds
pass number 46 took 65 milliseconds
pass number 47 took 64 milliseconds
pass number 48 took 65 milliseconds
pass number 49 took 122 milliseconds
pass number 50 took 103 milliseconds
pass number 51 took 45 milliseconds
pass number 52 took 77 milliseconds
pass number 53 took 64 milliseconds
pass number 54 took 96 milliseconds
..SNIP lots between 30-80ms...
pass number 77 took 44 milliseconds
pass number 78 took 85 milliseconds
pass number 79 took 142 milliseconds
pass number 80 took 138 milliseconds
pass number 81 took 47 milliseconds
pass number 82 took 44 milliseconds
..SNIP lots between 30-80ms...
pass number 93 took 52 milliseconds
pass number 94 took 50 milliseconds
pass number 95 took 63 milliseconds
pass number 96 took 111 milliseconds
pass number 97 took 175 milliseconds
pass number 98 took 96 milliseconds
pass number 99 took 67 milliseconds

内存使用量在最初创建数组时上升到刚刚超过 9GB,在 GC.Collect 之后下降到大约 6.5GB,在添加到字典时又上升到超过 9GB,然后全部完成(它正在等待在 console.Readline()) 上它会回落到 ~3.7GB 并保持在那里。

显然在操作上预分配字典要快得多。

供参考,原文如下*

我刚刚写了这个小测试。我不知道你在存储什么,所以我刚刚创建了一个没有太多信息的毫无意义的小类,并使用 int 作为键,但我从中得出的两个结论是:

  1. 它在添加到字典时似乎并没有逐渐变慢,直到它达到大约 4000 万个项目。运行针对 x64 的“发布”构建,每百万次插入大约需要 500 毫秒,然后从 41 到 46 大约需要 700-850 毫秒(此时明显跳跃)

  2. 它处理了超过 46,000,000 个项目,占用了大约 4GB 的 RAM,并因内存不足异常而崩溃。

  3. 使用数据库,否则滥用字典的小组会来取缔你。

代码:

class AThing
{
    public string Name { get; set; }
    public int id { get; set; }
}
class Program
{
    static void Main(string[] args)
    {
        Dictionary<int, AThing> dict = new Dictionary<int, AThing>();

        for (var c = 0; c < 100; c++)
        {
            DateTime nowTime = DateTime.Now;
            for (var i = 0; i < 1000000; i++)
            {
                var thing = new AThing { id = (c * 1000000) + i, Name = $"Item {(c * 1000000) + i}" };
                dict.Add(thing.id, thing);
            }
            var timeTaken = DateTime.Now - nowTime;
            Console.WriteLine($"pass number {c} took {timeTaken.Milliseconds} milliseconds");
        }

    }
}

【讨论】:

  • 我仍然想知道 OP 存储的是什么;如果是单字节图像还是 4K 图像,差别很大。
  • 只是一个小提示。使用System.Diagnostics.Stopwatch 测量执行时间。 stackoverflow.com/a/28648/4634044
  • @Wapac - 老实说,我也很惊讶.. 我只是想知道它是否会有明显的跳跃,它必须分配更多的内存或任何东西,但没想到它会这么快用完.显然,为了这个小例子,我认为这并不重要……但算一算,46,000,000 个 50 字节的实例无论如何都超过 2GB,所以在 100,000,000 时,你会很高兴超过 4GB。我并不是说不可能将所有这些数据保存在内存中以便快速访问...我只是认为 c# Dictionary 可能不适合使用。
  • @Wapac 我已经更新了示例。在 x64 模式下创建对象会消耗 相当多 更多内存,我猜是因为 64 位指针?它现在并没有耗尽内存,并且由于字典中的项目数量,它似乎没有直接失去性能。不知道查找可能会受到怎样的影响。
  • Dictionary 从未为此目的而设计。首先,您可能至少需要一个 64 位的哈希状态——假设您可以控制哈希计算。你应该使用一个好的散列函数——而不是内置的。你应该缓存每个条目的散列。其次, Dictionary 在质数上浪费了大量时间和空间。创建您自己的分层哈希表,使用两个表大小的幂,您可能会做得更好。
【解决方案2】:

我知道距离您最初提出这个问题已经三年了。但是我自己也遇到了同样的问题,并通过实现FixedSizeDictionary&lt;TKey, TValue&gt; 设法找到了解决方案,其中我将最大 size 作为int 传递,同时它不断向其中添加项目,在项目计数超过提供的固定值后,它还会删除最旧的

【讨论】:

  • 您的答案可以通过额外的支持信息得到改进。请edit 添加更多详细信息,例如引用或文档,以便其他人可以确认您的答案是正确的。你可以找到更多关于如何写好答案的信息in the help center
【解决方案3】:

如果希望程序在字典处于最大大小时工作,那么为什么不从一开始就将其分配到最大大小并完全避免重新索引等。使用的内存量只是暂时与其他解决方案不同,但节省的时间不是暂时的,另外,当字典处于空状态时,发生冲突的可能性非常低。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-06-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-01-01
    • 1970-01-01
    相关资源
    最近更新 更多