我的字典大小正常吗?

4
我可以帮您进行翻译。以下是需要翻译的内容:

我有一个150MB的文件。每一行都由相同的格式组成,例如/

I,h,q,q,3,A,5,Q,3,[,5,Q,8,c,3,N,3,E,4,F,4,g,4,I,V,9000,0000001-100,G9999999990001800000000000001,G9999999990000001100PDNELKKMMCNELRQNWJ010, , , , , , ,D,Z,

我有一个 Dictionary<string, List<string>>

它通过打开文件,读取每一行,从行中取出元素并添加到字典中来填充,然后关闭文件。

StreamReader s = File.OpenText(file);
 string lineData = null;
 while ((lineData = s.ReadLine()) != null)
 {
   var elements = lineData.Split(',');
   var compareElements = elements.Take(24);
   FileData.Add(elements[27], new List<string>(compareElements));

  }
  s.Close();

使用这个答案中的方法,我计算出我的字典大小为600mb。这是文件大小的4倍。

听起来正确吗?


你的文件使用的是什么编码? - Paolo Moretti
1
虽然与您的问题没有直接关系,但您应该确保处理任何实现了IDisposable接口的东西。在您的情况下,您不仅应该调用s.Close(),还应该调用s.Dispose()或者将StreamReader包装在using块中。 - Kevin Wienhold
谢谢Kevin,我认为同样适用于StreamWriter。 - Jon
@Jon:是的,这也适用于StreamWriter。我最期望的Visual Studio未来功能之一是对实现IDisposable接口的类型进行某种形式的高亮显示,目前,你最好的选择是查看给定类是否有Dispose()方法。 - Kevin Wienhold
6个回答

3
除了这种方法不太可靠之外,在你的情况下,还存在更大的开销。你是否注意到,每次循环迭代都会创建一个新实例的elements数组、lineData字符串,以及在每次调用时创建一些内部变量的elements.Take?由于你可能有足够的RAM,.NET垃圾收集器不会费心收集它们,因此当你在循环前后测量TotalMemory时,你也测量所有这些变量,而不仅仅是你的词典,尽管它可能是唯一留在范围内的东西。

元素方面的观点很好。可以将其移出while循环。那么你会怎么处理lineData? - Jon
什么?不,你不能在循环外移动它!每次调用 Split() 都会产生一个新的数组;即使你将其分配给同一变量,也没有关系。旧的数组直到垃圾收集器决定进行清理时才会被清除出内存。对于 lineData 也是如此 - 每次调用 ReadLine() 都会产生一个新的字符串实例。这是无法避免的事实。 - Vilx-
二进制格式化程序输出的大小与存储在堆上时的大小之间几乎没有可证明的关系。如果有什么区别,我不会感到惊讶,如果List<string>的二进制序列化结果比内存表示更紧凑一个数量级。 - sehe
@Vilx-:我已经运行了这个小的C#紧密循环(IL代码显示)快速,小型分配(Gen0)超过29分钟了,它从未增长过。我有8Gbs的RAM(64位操作系统),其中> 50%是空闲的。我不知道.NET垃圾收集的确切机制,但似乎可以安全地假设它不会等到无聊之前优化Gen0分配。当然,该循环紧密绑定100%的CPU。 - sehe
好奇。嗯,我猜他们已经让GC比我知道的更聪明了。 :P 不过,当试图测量事物时,我不会依赖于此。我们所知道的是,在您的机器和OP的机器上可能存在其他隐藏因素(例如.NET版本,或者可能是免费的CPU核心或其他因素)可能会有所不同。 - Vilx-
显示剩余2条评论

1

大多数这些实体只需要一个字符,但您将它们存储为字符串。对于那些字符串的引用指针单独将占用至少两倍的空间(在UTF8的情况下可能是4-8倍)。然后还有维护字典哈希表的开销。

List<>本身在存储方面应该非常高效(它在内部使用数组)

改进的空间

  • 如果您知道字段将适合,可以使用List<char>char[]代替List<string>
  • 如果需要每个字段超过1个字符,可以使用struct Field { char a,b/*,...*/; }和List代替List
  • 您可以放弃急切的字段提取[<--建议]:

     var dict = File.ReadAllLines(file)
          .ToDictionary(line => line.Split(',')[27]);
    

    这使您有机会按需访问compareElements:

     string[] compareElements = dicts["key27"].Split(',')/*.Take(24).ToArray()*/;
    

    这是运行时/存储成本权衡的典型示例

编辑一个明显的混合体将是:

struct AllCompareElements
{
     public char field1, field2, ... field24;
     // perhaps:
     public char[2] field13; // for the exceptional field that is longer than 1 character
}

愉快地使用 Resharper 实现 EqualsGetHashCodeIEquatable<AllCompareElements>IComparable<AllCompareElements>

如果我使用char[],我将不得不删除逗号。这不是问题,只是说一下,但它会增加额外的负担,而string.Split已经为我完成了这个任务。 - Jon
@Jon:你误解了我的意思。我是指将分割的结果存储在List<char>中,而不是List<string>中:line.Split(',')。Take(24).Select(s => s [0]);。不仅char通常比string小,更重要的是,char是“ValueType”,而string是“reference type”。特别是在将valuetype存储在泛型List<>中时,存储需求得到了极大的优化。 - sehe
@Jon:啥?ReadAllLines只是你自己的while循环的缩写。请注意,您获取“比较元素”的前24个(从拆分的逗号分隔字段中提取),而不是输入文件中的行的前24个? - sehe
我使用Take(24)的原因是因为我在程序的另一部分中使用了该字典,只需要这24个元素。 - Jon
@Jon 我猜到了 :) 你的评论 ...只需要存储24个字段,所以只需执行ReadAllLines... 让我感到困惑。此外,重点是您可能会获得更好的存储效率。当然,这取决于这24个字段之后有多少数据,但是有中间方法。主要是,单个字符串将比具有24个元素的List<string>具有更好的存储效率。这是事实。 - sehe
显示剩余3条评论

1

是的,因为你正在将字符转换为字符串指针,每个指针占据4或8个字节。


1

我假设你的文件是以UTF-8编码并且主要包含ASCII字符。在C#中,字符串是UTF-16编码的,因此这就解释了大部分大小差异(2倍因子)。当然,数据结构也会有一些开销。


1

0

这600M是通过将文件加载到字典中分配的... 这表明它是一项昂贵的操作,可以用来衡量任何优化的效果,但对于字典占用多少内存而言,相当无用。

我会像sehe建议的那样立即推迟拆分。

在我看来,你已经过早地优化了速度,这在内存占用方面给你带来了巨大的代价。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接