将大文件读入字典

时间:2008-12-05 14:24:26

标签: c# performance memory file filesystems

我有一个包含字符串和长对的1GB文件。 将它读入字典的最佳方式是什么,你说它需要多少内存?

文件有6200万行。 我已经设法使用5.5GB的ram读取它。

每个Dictionary条目说22个字节的开销,即1.5GB。 long是8个字节,即500MB。 平均字符串长度为15个字符,每个字符2个字节,即2GB。 总计大约4GB,额外的1.5 GB到哪里去了?

初始字典分配需要256MB。 我注意到我读取的每1000万行消耗大约580MB,这与上面的计算完全吻合,但在6000行左右,内存使用量从260MB增加到1.7GB,这是我缺少的1.5GB,它在哪里去吗?

感谢。

10 个答案:

答案 0 :(得分:13)

了解填充Hashtable时发生的情况非常重要。 (The Dictionary使用Hashtable作为其底层数据结构。)

当你创建一个新的Hashtable时,.NET会生成一个包含11个桶的数组,这些桶是字典条目的链接列表。当您添加一个条目时,它的键被哈希,哈希代码被映射到11个桶中的一个,并且条目(键+值+哈希码)被附加到链表。

在某一点(这取决于首次构造Hashtable时使用的负载因子),Hashtable在Add操作期间确定它遇到了太多的冲突,并且最初的11个桶不够。因此它创建了一个新的桶数组,其大小是旧数据块的两倍(不完全是;桶的数量总是为素数),然后从旧表中填充新表。

因此,在内存利用率方面有两件事情可以发挥作用。

首先,Hashtable经常需要使用两倍于目前使用的内存,因此它可以在调整大小期间复制表。所以,如果你有一个使用1.8GB内存的Hashtable并且它需要调整大小,那么它需要使用3.6GB,现在你有问题。

第二个是每个哈希表条目都有大约12个字节的开销:指向密钥的指针,值和列表中的下一个条目,加上哈希码。对于大多数用途,这种开销是微不足道的,但如果你正在构建一个包含1亿条目的Hashtable,那么这大约是1.2GB的开销。

您可以通过使用Dictionary的构造函数的重载来解决第一个问题,该构造函数可以提供初始容量。如果您指定的容量足以容纳您将要添加的所有条目,则在填充Hashtable时不需要重建Hashtable。对于第二种情况,你几乎无能为力。

答案 1 :(得分:9)

这里的每个人似乎都同意,处理此问题的最佳方法是一次只将一部分文件读入内存。当然,速度取决于哪个部分在内存中,以及在需要特定信息时必须从磁盘读取哪些部分。

有一种简单的方法可以处理决定保留在内存中的最佳部分:

将数据放入数据库。

真实的,如MSSQL Express,MySql或Oracle XE(都是免费的)。

数据库缓存最常用的信息,因此就像从内存中读取一样。它们为您提供内存或磁盘数据的单一访问方法。

答案 2 :(得分:5)

也许您可以将1 GB文件转换为具有两列键和值的SQLite数据库。然后在键列上创建索引。之后,您可以查询该数据库以获取您提供的密钥的值。

答案 3 :(得分:4)

想到这一点,我想知道为什么你需要这样做...(我知道,我知道......我不应该想知道为什么,但是听我说出来......)

主要问题是需要大量数据需要快速访问...问题是,它本质上是随机访问,还是有一些模式可以被利用来预测访问? / p>

在任何情况下,我都会将其实现为滑动缓存。例如。我会尽可能多地加载到内存中(尽可能选择基于我期望的访问模式加载的内容),然后跟踪上次访问时的元素访问。 如果我点击了缓存中没有的内容,那么它将被加载并替换缓存中最旧的项目。

这会导致最常用的内容可以在内存中访问,但会导致缓存未命中的额外工作。

无论如何,在不了解问题的情况下,这只是一个“通用解决方案”。

可能只是将它保存在sql db的本地实例中就足够了:)

答案 4 :(得分:2)

您需要指定文件格式,但如果它只是名称=值,我会这样做:

Dictionary<string,long> dictionary = new Dictionary<string,long>();
using (TextReader reader = File.OpenText(filename))
{
    string line;
    while ((line = reader.ReadLine()) != null)
    {
        string[] bits = line.Split('=');
        // Error checking would go here
        long value = long.Parse(bits[1]);
        dictionary[bits[0]] = value;
    }
}

现在,如果这不起作用,我们需要更多地了解该文件 - 有多少行等等?

您使用的是64位Windows吗? (如果没有,那么无论如何,每个进程都不能超过3GB,IIRC。)

所需的内存量取决于字符串的长度,条目数等。

答案 5 :(得分:1)

我不熟悉C#,但如果您遇到内存问题,可能需要为此任务滚动自己的内存容器。

由于你想将它存储在一个字典中,我认为你需要它来快速查找? 但是,你还没有明确哪一个应该是关键。

我们希望您想要使用密钥的长值。然后试试这个:

分配与文件一样大的缓冲区。将文件读入该缓冲区。

然后创建一个包含长值(32位值,我猜?)作为键的字典,其值也是32位值。

现在浏览缓冲区中的数据,如下所示: 找到下一个键值对。计算缓冲区中其值的偏移量。现在将此信息添加到字典中,其中long为键,偏移量为其值。

这样,你最终会得到一个字典,每个记录可能占用10-20个字节,还有一个较大的缓冲区可以保存所有文本数据。

至少在C ++中,我认为这将是一种节省内存的方式。

答案 6 :(得分:1)

您可以将1G文件转换为更高效的索引格式,但将其作为文件保留在磁盘上吗?然后,您可以根据需要访问它并进行有效的查找。

也许你可以通过内存映射这个(更有效的格式)文件的内容,然后有最小的ram使用和需求加载,这可能是在光盘上直接访问文件和加载之间的良好平衡。整个事情变成一个大字节数组。

答案 7 :(得分:0)

一次在内存中加载1 GB文件对我来说听起来不是一个好主意。我只是在需要特定块时才将其加载到较小的块中,从而虚拟化对文件的访问。当然,它比将整个文件放在内存中要慢,但1 GB是一个真正的乳齿象...

答案 8 :(得分:0)

即使您有8 GB的物理RAM,也不要将1GB的文件读入内存,但仍然会遇到很多问题。 - 基于个人经验 -

我不知道你需要做什么,但找到一个解决方法并部分阅读和处理。如果它不起作用,那么考虑使用数据库。

答案 9 :(得分:0)

如果您选择使用数据库,那么dbm风格的工具(如Berkeley DB for .NET)可能会更好。它们专门用于表示基于磁盘的哈希表。

或者,您可以使用某些数据库技术推出自己的解决方案。

假设您的原始数据文件如下所示(点表示字符串长度不同):

[key2][value2...][key1][value1..][key3][value3....]

将其拆分为索引文件和值文件。

值文件:

[value1..][value2...][value3....]

索引文件:

[key1][value1-offset]
[key2][value2-offset]
[key3][value3-offset]

索引文件中的记录是固定大小的key->value-offset对,按键排序。 值文件中的字符串也按键排序。

要获取key(N)的值,您将在索引中二进制搜索key(N)记录,然后从值value(N)-offset开始到value(N+1)-offset之前的值文件中读取字符串。

索引文件可以读入内存中的结构数组(比字典更少的开销和更可预测的内存消耗),或者你可以直接在磁盘上进行搜索。