【问题标题】:Reading a large file into a Dictionary将大文件读入字典
【发布时间】:2010-09-25 12:44:05
【问题描述】:

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

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

假设每个字典条目有 22 个字节的开销,即 1.5GB。 long 是 8 个字节,即 500MB。 平均字符串长度为 15 个字符,每个字符 2 个字节,即 2GB。 总共大约 4GB,多出来的 1.5GB 去哪儿了?

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

谢谢。

【问题讨论】:

    标签: c# performance memory file filesystems


    【解决方案1】:

    您需要指定文件格式,但如果它只是 name=value 之类的,我会这样做:

    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 吗? (如果没有,IIRC 无论如何每个进程都不能使用超过 3GB 的空间。)

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

    【讨论】:

    • 我认为 3.5GB 是整个系统将使用的物理内存量,但每个进程限制为 3GB。无论哪种方式,它都小于 5 :)
    • 并且您的应用程序需要设置为任何 cpu 或 x64 才能利用 64 位系统。
    • 32 位在没有 PAE 的情况下仍然有每个进程 2GB 的限制。 3GB 开关只是意味着所有应用程序共享 3GB,内核使用 1GB。
    【解决方案2】:

    考虑到这一点,我想知道您为什么需要这样做...(我知道,我知道...我不应该想知道为什么,但请听我说...)

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

    无论如何,我会将其实现为滑动缓存。例如。我将尽可能多地加载到内存中(尽可能根据我预期的访问模式选择要加载的内容),然后按上次访问的时间跟踪对元素的访问。 如果我碰到了不在缓存中的东西,那么它将被加载并替换缓存中最旧的项目。

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

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

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

    【讨论】:

    • 1 GB 是获得任何性能改进的最低要求(在利用所有可能的访问模式之后)。实际上,我真的在考虑一个内存数据库。
    • 好吧,在这种情况下,您可以尝试上述方法...稍作调整,它可能会满足您的需求。 :) 做不到这一点,把它放在一个本地数据库实例中,让它负责缓存。
    【解决方案3】:

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

    【讨论】:

    • 这对您来说可能听起来不是一个好主意,但您敢打赌这对 Google 来说是个好主意,因为这正是他们正在做的事情。将所有索引存储在内存中。
    • @lubos hasko:如果您拥有 Google 数据中心,可能会更容易。但是假设每个客户都有一个听起来仍然不是一个好主意。
    【解决方案4】:

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

    【讨论】:

      【解决方案5】:

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

      有一种简单的方法可以决定哪些部分最好保存在内存中:

      将数据放入数据库。

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

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

      【讨论】:

        【解决方案6】:

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

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

        【讨论】:

          【解决方案7】:

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

          既然你想将它存储在一个字典中,我假设你需要它来快速查找? 不过,您还没有明确哪一个应该是关键。

          希望您希望对键使用长值。然后试试这个:

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

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

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

          这样,您最终会得到一个字典,每条记录可能需要 10-20 个字节,以及一个更大的缓冲区来保存您的所有文本数据。

          我认为,至少对于 C++,这将是一种相当节省内存的方式。

          【讨论】:

            【解决方案8】:

            了解填充 Hashtable 时发生的情况很重要。 (字典使用 Hashtable 作为其底层数据结构。)

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

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

            因此,就内存利用率而言,有两件事会发挥作用。

            首先,Hashtable 每隔一段时间就需要使用两倍于当前使用量的内存,以便它可以在调整大小时复制表。因此,如果您有一个使用 1.8GB 内存的 Hashtable,并且需要调整它的大小,那么它短暂需要使用 3.6GB,那么,现在您遇到了问题。

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

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

            【讨论】:

            • 我已经创建了具有足够大初始容量的字典。我还可以看到内存使用量增长缓慢,没有任何大的跳跃。
            【解决方案9】:

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

            也许你可以内存映射这个(更有效的格式)文件的内容,然后有最小的内存使用和需求加载,这可能是一个很好的权衡,在一直直接访问磁盘上的文件和加载整个东西变成一个大字节数组。

            【讨论】:

              【解决方案10】:

              如果您选择使用数据库,最好使用 dbm 风格的工具,例如 Berkeley DB for .NET。它们专门用于表示基于磁盘的哈希表。

              您也可以使用一些数据库技术推出自己的解决方案。

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

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

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

              值文件:

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

              索引文件:

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

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

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

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

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 2021-03-06
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多