簡體   English   中英

用於大量項目的 C# 字典

[英]C# dictionary for large number of items

我想了解在 C# 中將大量項目存儲在內存中的成本。 我需要使用的數據結構是字典或類似的。 假設我想要的項目數量約為 1 億,但應用程序不會立即達到該數量。 我們需要很長時間才能達到極限。

我擔心攤銷的運營成本,但在任何時候我都不能承受過高的成本。 所以通常使用動態數據結構,當結構已滿時,它會重新分配自己。 在字典的情況下,我認為它甚至會重新索引每個項目。 因此,假設我們是應用程序維護 2000 萬個剛剛達到字典容量的項目的重點。 然后,當分配新的字典存儲時,需要重新索引這 2000 萬個項目。

這就是為什么我認為一系列字典可能是個好主意的原因。 假設我創建了 256 個字典。這立即將每個內部字典的大小限制為少於 100 萬個項目,這應該是易於管理的,並且所有索引都發生在多達 100 萬個項目的過程中。 這樣做的成本似乎只是每次操作一個額外的索引以找到要查看的正確字典。

這是一個合理的方法嗎? 我的分析是正確的還是我認為 C# 字典會因為某種原因表現得更好? 有沒有其他更好的解決方案? 我正在尋找一種與 C# 字典具有相同時間復雜度的數據結構。

編輯:字典鍵是一個隨機值,所以我可以用它的第一口來非常便宜地找到我在 256 個字典數組中的索引。

我目前不考慮數據庫,因為我希望所有項目都可以立即使用,而且成本很低。 我確實需要以很少的開銷在恆定時間內查找。 我可以承受插入速度變慢,但仍然是恆定的時間。 與刪除相同,可以慢一點,但需要恆定的時間。

應該可以容納內存中的所有項目。 這些項目很小,每個大約 50 字節的數據。 所以數據結構對於每一項都不能有太多的開銷。

更新:自發布以來,我對此進行了編輯:

  • 存儲固定大小的對象(每次通過時,字節[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批次在30-80ms之間...通過號77花費了44毫秒通過號78花費了85毫秒通過號79花費了142毫秒通過號80花費138毫秒通過數81花費了47毫秒通過號82花費了44毫秒..SNIP介於30到80毫秒之間...通過號93花費52毫秒通過號94花費50毫秒通過號95花費63毫秒通過號96花費111毫秒通過號97花費175毫秒通過號98花費96毫秒通過號99花費67毫秒

在最初創建陣列時,內存使用量上升到9GB以上,在GC.Collect之后下降到大約6.5GB,在添加到字典中后又上升到9GB以上,然后完成所有操作(在控制台上等待) .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");
        }

    }
}

如果希望程序在字典最大大小的時候運行,那么為什么不從一開始就將其分配給最大大小,又避免完全重新編制索引。 所使用的內存量只是暫時不同於其他解決方案,但是節省的時間不是暫時的,而且,當字典處於空狀態時,發生沖突的可能性非常低。

我知道距離你最初問這個問題已經三年了。 但是我自己也遇到了同樣的問題,並設法通過實現FixedSizeDictionary<TKey, TValue>找到了解決方案,在其中我將最大大小作為int傳遞,並且在不斷向其中添加項目的同時,它還將刪除最舊的在項目計數通過提供的固定值之后。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM