【问题标题】:Binary Search on Large Disk File in C - ProblemsC中大型磁盘文件的二进制搜索 - 问题
【发布时间】:2013-02-15 14:59:05
【问题描述】:

这个问题在 StackOverflow 上经常出现,但是我已经阅读了之前所有的相关答案,并且对这个问题有一点曲解。

我有一个 23Gb 的文件,其中包含 4.75 亿行大小相等的行,每行包含一个 40 个字符的哈希码,后跟一个标识符(一个整数)。

我有一个传入的哈希码流 - 总共有数十亿个 - 对于每个传入的哈希码,我需要找到它并打印出相应的标识符。这项工作虽然很大,但只需要完成一次。

文件太大,我无法读入内存,所以我一直在尝试通过以下方式使用 mmap:

codes = (char *) mmap(0,statbuf.st_size,PROT_READ,MAP_SHARED,codefile,0); 

然后我只是根据代码中的地址使用地址算法进行二进制搜索。

这似乎开始工作很漂亮,并在几秒钟内产生了几百万个标识符,使用了 100% 的 cpu,但在经过一些看似随机的时间后,它会减慢到爬行。当我使用 ps 查看进程时,它已从使用 100% 的 cpu 的状态“R”变为使用 1% 的 cpu 的状态“D”(磁盘绑定)。

这是不可重复的 - 我可以在相同的数据上再次启动该过程,并且它可能会运行 5 秒或 10 秒,然后才会发生“爬行缓慢”。昨晚有一次,在这件事发生之前我有将近一分钟的时间。

一切都是只读的,我没有尝试对文件进行任何写入,并且我已经停止了机器上的所有其他进程(我控制的)。它是现代 Red Hat Enterprise Linux 64 位机器。

有谁知道这个进程为什么会被磁盘绑定以及如何停止它?

更新:

感谢大家的回答和您的想法;我以前没有尝试过所有各种改进,因为我想知道我是否以某种方式错误地使用了 mmap。但答案的要点似乎是,除非我能将所有内容都挤进内存,否则我将不可避免地遇到问题。所以我将散列码的大小压缩到不产生任何重复的前导前缀的大小——前 15 个字符就足够了。然后我将生成的文件拉入内存,并以大约 20 亿个批次运行传入的哈希码。

【问题讨论】:

  • 您遇到了虚拟内存的限制。虽然活动数据集适合内存,但一切都是正常的;当您需要对太大而无法放入内存的数据集进行随机访问时,您开始进行硬分页,成为磁盘绑定。没有一个简单的方法可以解决这个问题。整数是存储为 4 字节或 8 字节二进制值还是字符串?是否有可能重组 23 GiB 文件?
  • @paxdiablo:问题不在于虚拟内存大小的限制;问题是底层物理内存。该程序仍在运行,但是当物理内存为 16 GiB 时,随机访问超过 23 GiB 的数据,这意味着一旦你在内存中获得了 2/3 的文件,此后,平均而言,你必须三分之二读取一个内存访问的新页面,这非常慢。
  • @JonathanLeffler:是的,我在重读后意识到你的意思(因此我删除了评论)。什么让我断言这是虚拟内存的限制(我将其读为大小)而不是虚拟内存管理(即映射,正如您在回复中解释的那样)。跨度>
  • 你有多少内存?具体来说,您一次可以在内存中保存多少个传入哈希码(并排序)?您能否反转问题,按顺序读取文件并将其与传入数据进行匹配,而不是反之亦然?

标签: c linux binary-search


【解决方案1】:

首先要做的是拆分文件。

用哈希码制作一个文件,用整数 id 制作另一个文件。由于行相同,因此在找到结果后它将很好地排列。您也可以尝试将每个第 n 个哈希放入另一个文件然后存储索引的方法。

例如,每 1000 个哈希键放入带有索引的新文件中,然后将其加载到内存中。然后二进制扫描它。这将告诉您需要在文件中进一步扫描的 1000 个条目的范围。是的,那会做得很好!但可能远不止于此。可能每 20 条左右的记录就会将该文件大小除以 20 +- 如果我认为不错的话。

换句话说,扫描后您只需要触摸磁盘上几千字节的文件。

另一种选择是将文件拆分并放在多台机器上的内存中。然后只需二进制扫描每个文件。这将在零磁盘访问的情况下产生绝对最快的搜索...

【讨论】:

  • 拆分 40 字节的哈希码和 4 或 8 字节的整数可能效果不大,您说的是减少了 9 或 17%。您的第二个建议似乎更有希望。
  • 我接受了这个答案,因为它包含两个好主意,即使它没有涵盖其他答案的所有方面。
【解决方案2】:

您是否考虑过破解 PATRICIA trie 算法?在我看来,如果您可以构建数据文件的 PATRICIA 树表示,它指的是哈希值和整数值的文件,那么您可以将每个项目减少为节点指针(2*64 位?),位测试偏移量(本场景中为 1 字节)和文件偏移量(uint64_t,可能需要对应多个 fseek())。

【讨论】:

    【解决方案3】:

    有谁知道这个进程为什么会被磁盘绑定以及如何停止它?

    二分查找需要在文件中进行大量查找。在整个文件不适合内存的情况下,页面缓存不能很好地处理大搜索,从而导致您看到的行为。

    解决此问题的最佳方法是减少/防止大搜索并使页面缓存为您工作。

    给你的三个想法:

    如果您可以对输入流进行排序,您可以使用类似于以下算法的方法分块搜索文件:

    code_block <- mmap the first N entries of the file, where N entries fit in memory
    max_code <- code_block[N - 1]
    while(input codes remain) {
      input_code <- next input code
      while(input_code > max_code)  {
        code_block <- mmap the next N entries of the file
        max_code <- code_block[N - 1]
      }
      binary search for input code in code_block
    }
    

    如果您无法对输入流进行排序,您可以通过构建数据的内存索引来减少磁盘寻道。传递大文件,并制作一个table,即:

    record_hash, offset into file where this record starts
    

    不要在此表中存储所有记录 - 仅存储每第 K 条记录。选择一个大的 K,但要足够小,以适应内存。

    要在大文件中搜索给定的目标哈希,请在内存表中进行二进制搜索,以找到table 中小于目标哈希的最大哈希。假设这是table[h]。然后,mmap 从table[h].offsettable[h+1].offset 结束的段,并进行最终的二分查找。这将大大减少磁盘寻道次数。

    如果这还不够,你可以有多层索引:

     record_hash, offset into index where the next index starts
    

    当然,您需要提前知道有多少层索引。


    最后,如果你有多余的钱,你总是可以购买超过 23 GB 的 RAM,这又是一个内存绑定问题(我刚刚查看了戴尔的网站 - 你买了一个新的配备 32 GB RAM 的低端工作站,售价略低于 1,400 澳元)。当然,从磁盘中读取这么多数据需要一段时间,但一旦数据到位,您就可以设置好了。

    【讨论】:

      【解决方案4】:

      不要使用mmap,而是考虑使用普通的旧lseek+read。您可以定义一些辅助函数来读取哈希值或其对应的整数:

      void read_hash(int line, char *hashbuf) {
          lseek64(fd, ((uint64_t)line) * line_len, SEEK_SET);
          read(fd, hashbuf, 40);
      }
      
      int read_int(int line) {
          lseek64(fd, ((uint64_t)line) * line_len + 40, SEEK_SET);
          int ret;
          read(fd, &ret, sizeof(int));
          return ret;
      }
      

      然后像往常一样进行二进制搜索。它可能会慢一点,但它不会开始占用您的虚拟内存。

      【讨论】:

      • +1。这仍然会填充页面缓存,这在技术上是内核 VM 子系统的一部分。但是,根据我的经验,VM 子系统处理未映射页面上的压力比处理 mmap 页面上的压力要好。这种方法可能无济于事,但很容易尝试。
      • 这很好,但是将它与内存中的部分有序映射结合起来会更好。换句话说,不要执行 24 或 16 次磁盘读取来定位记录,而是使用部分搜索。换句话说,例如每 64 条记录,这极大地缩小了范围。然后在更小的范围内使用这种磁盘访问技术。
      • 是的,我同意。我认为减少 I/O 总量至关重要,部分映射是一个很好的方法。
      • 哈,我从 lseek 移到了 mmap,因为这也绑定了磁盘。
      • 为什么你认为lseek 会比mmap 快?我不这么认为。
      【解决方案5】:

      我们不知道背后的故事。所以很难给你明确的建议。你有多少内存?你的硬盘有多复杂?这是一个学习项目吗?谁为你的时间买单?与每小时 50 美元的两天工作相比,32GB 的内存似乎并不昂贵。这需要运行多快?你愿意走出多远?您的解决方案是否需要使用高级操作系统概念?你嫁给了 C 语言的程序吗?让 Postgres 处理这个怎么样?

      这是一个低风险的选择。此选项在智力上不如其他建议那么吸引人,但有可能为您带来显着收益。将文件分成 3 个 8GB 块或 6 个 4GB 块(取决于您周围的机器,它需要舒适地放入内存中)。在每台机器上运行相同的软件,但在内存中并在每个机器周围放置一个 RPC 存根。为 3 或 6 个工作人员中的每一个编写一个 RPC 调用程序,以确定与给定哈希码关联的整数。

      【讨论】:

      • 所有优点......事实证明,这台机器已经有 32Gb RAM(这引出了为什么它首先被磁盘绑定的问题)。我需要我的一个研究项目的数据,但创建一个数据集是一次性的,我随后将存储在数据库中,所以它不是一个专门的编程学习项目,尽管我需要学习足够多的东西来做它并且将来可能会做类似的事情。语言是 C,因为这是我最了解的..
      猜你喜欢
      • 1970-01-01
      • 2021-09-18
      • 2017-03-04
      • 2013-09-19
      • 1970-01-01
      • 2014-11-02
      • 2022-01-05
      • 2011-09-16
      • 1970-01-01
      相关资源
      最近更新 更多