给定磁盘上的1 TB数据集,每个数据记录大约1 KB,如何使用512 MB RAM和无限磁盘空间查找重复项?

时间:2010-04-04 05:21:46

标签: c++ algorithm data-structures

磁盘上有1 TB数据,每个数据记录大约1 KB。如何使用512 MB RAM和无限磁盘空间找到重复项?

8 个答案:

答案 0 :(得分:19)

到目前为止提供的解决方案似乎过于复杂。 Bloom filter虽然是过去几年的数据结构 du jour ,但在这种情况下并不是最好的应用:因为没有数据可以与散列内容相关联,所以不仅必须维护Bloom过滤器,还必须记录每个(仅6位!)哈希值并记录到磁盘,破坏布隆过滤器的好处并具有非常高的冲突率。

另一方面,对整个TB进行合并排序不仅要进行O(n log n)比较,还要进行O(n log n)磁盘流量,因为大多数中间文件必须从磁盘合并,而不是记忆。任何真正的解决方案都应该尽可能地减少磁盘流量,因为这是我们的主要瓶颈。

我的解决方案很简单,做出一个假设:将数TB的数据记录在实际上是一个文件中。

迭代terabyte文件的记录并散列它们。加密哈希在这里是不必要的,昂贵的和太大的;相反,使用64-bit version of murmurhash之类的东西。它可以散布超过2 GiB /秒(比我们可能需要的速度快得多,因为这些天的存储速度)并且具有出色的(尽管不是加密安全的)碰撞阻力。使用64位散列we would expect our first collision at 2^32,所以我们大约10亿条记录很可能根本不会发生任何冲突。

将散列及其关联的记录偏移写入另一个文件。由于记录包含任意二进制数据,因此我们不能依赖Unix的sort(1)对其进行排序,因为某些散列和偏移可能包含sort(1)将解释为换行符。我们将简单地将记录写为固定宽度(可能是16个字节:murmur2 64位散列为8个字节,terabyte文件中的偏移量为8个字节)记录。根据我们的记录数量,生成的文件大约应为16 GB。

我们可以通过读取安全地放入内存并对其进行排序的记录数量来排序此文件,将已排序的块刷新回磁盘。我们可以使用heapsort(它使用O(1)空间)将更多记录放入内存中,而不是使用quicksort(使用O(log n)内存用于调用堆栈),但在大多数实现中,quicksort凭借其获胜而获胜内存局部性和较低的指令数。这些中间文件(应该有35-40个)将写入磁盘。

最后一步是合并这些文件(在内存中;不需要在磁盘上存储结果)收集所有哈希冲突并查找terabyte文件中的相关记录,比较重复记录和发送记录(或其偏移量)以问题指定的任何方式。

据我所知,这个任务比任何其他提供的解决方案都要少得多,而且它在概念上非常简单:哈希记录,在哈希中查找重复项,并在实际记录中进行验证。

对于磁盘I/O,它将读取TB级数据文件,将16 GB写入磁盘,从磁盘读取16 GB并将其重新分类,然后读取并返回重复项。作为优化,散列记录的进程可以在将它们刷新到磁盘之前将它们累积到内存中,然后在执行此操作之前对它们进行排序:切断16 GB中间文件,并允许进程从散列直接转移到合并和报告重复项

答案 1 :(得分:18)

使用Bloom filter:同时哈希表。根据维基百科,哈希的最佳数量为ln(2) * 2^32 / 2^30 ≈ 2.77 ≈ 3。 (嗯,插入4会产生较少的误报,但3对于此应用程序仍然更好。)这意味着你有一个512兆字节或4千兆位的表,处理每个记录在这个巨大的海域设置三个新位。如果已经设置了所有三个位,那么这是一个潜在的匹配。将三个哈希值记录到文件中。否则,将它们记录到另一个文件。记下记录索引以及每个匹配。

(如果可以容忍5%的错误率,请省略大文件并使用小文件作为结果。)

完成后,您应该有一个大约49M可能的正匹配文件和一个975M负数的文件,但可能与肯定匹配。将前者读入vector<pair<vector<uint32_t>,vector<uint32_t> > >(后者vector中的索引,前者可以是array)并对其进行排序。将索引放在另一个vector<uint32_t>中;他们已经分类了。读取大文件,但不是在表中设置位,而是在vector中找到哈希值。 (例如,使用equal_range。)使用正文件索引列表来跟踪否定文件中当前记录的索引。如果未找到匹配项,请忽略。否则,追加记录的索引match->second.push_back(current_negative_record_index)

最后,遍历地图和记录索引的向量。任何具有多个条目的存储桶“几乎”肯定包含一组重复项,但您已经走到这一步,所以请查看它们并完全比较它们以确保。

总同步磁盘I / O :(一次通过= 1 TiB)+(每条记录96个哈希位= 12 GiB)+(每个正32个索引位= ~200 MiB)。

最终修改(严肃地说):第二个想法,Bloom Filter方面可能并没有真正帮到这里。散列数据的数量更多是误报数量的限制因素。仅使用一个散列函数,散列数据的总量将为4 GiB,并且1.24亿个预期误报的索引将为~500 MiB。这应该在全球范围内优化这一战略。

澄清(得到了一个downvote):Bloom过滤器的误报和哈希冲突之间存在区别。除非返回原始记录并进行比较,否则无法解决哈希冲突,这很昂贵。可以通过返回原始哈希值并比较它们来解决Bloom误报,这是该算法的第二次传递。因此,在第二个想法,“最终”编辑中描述的单哈希过滤器将不适当地导致磁盘搜索。双哈希布隆过滤器会增加在match映射的单个桶中结束的误报数量,并且会使误报数量减少到数千万。

答案 2 :(得分:13)

这是很多记录;-)大约1,000,000,000。最好是聪明一点......

记录的性质未指定:我们是否只是通过顺序读取它们来发现它们,或者是否有某种索引,或者它们是否存储在各种目录中的文件中?问题中未指定的是dbms的可用性,我们可以将其用于类索引数据(而不必使用我们自己的代码对其进行排序)。对重复数量的[甚至粗略]想法也有助于将一些选择引向有效的过程。

如果不存在索引,我们可以/应该创建一个索引;这可以在第一次通过数据时完成。相同的传递将用于为每个记录生成各种类型的消息摘要(散列)(或者为了效率目的,可能为记录的前几百个字节)。

一般的想法是快速生成一个索引,可用于识别可能的重复项,并最终确定实际重复列表,可能通过并行处理。< / p>

索引中有用的信息是:

  • 记录长度
  • 文本的前几个字节
  • 哈希码(更多内容见下文)
  • 也是文件中的偏移或任何指向数据的指针,但当然与上述3个元素不同,这不能用于识别潜在的匹配。

散列的选择至关重要:应该以牺牲完全分布的算法为代价来支持快速算法;每个记录的散列字节数也是一个折衷方案,可能100到200个字节(即大约10到20%的平均记录大小)是一个很好的值,取决于重复的预期比例,并取决于节省时间这提供了(与散列整个记录相比)。 (见下面的编辑)

一旦这样的索引可用,我们可以[相对快速/毫不费力地]获得可能重复的计数;基于这一结果,可以完成旨在提高指数质量的第二次通过(如果认为不够有选择性)(省略了容易被认为是唯一的记录)。该第二遍可以在整个记录(不包括第一个散列的前x个字节)上或在记录的另一个子集上计算另一个散列。请注意,由于索引,如果可能,第二遍可以是多线程的。

第二次或最后一次传递需要在一组可能的匹配(相同长度,相同的哈希码,相同的前x个字节)内对记录进行排序。这可以通过Pax Diablo描述来实现,索引的优点是这样的操作可以再次是多线程的并且涉及更小的集合(其中许多)。 已添加:此处 Nick Johnson 再次指出,如果我们使用长哈希码(他建议长度为128字节的SHA1),第二遍可能是不必要的。假设部分散列记录没有任何好处,这是一个非常合理的解决方案,因为索引可以驻留在磁盘上,但是比我们整理/存储整个记录时更快地排序和存储。


编辑 Nick Johnson 表明,磁盘存储中的搜索延迟可能会使得简单的顺序读取更快,而瓶颈就是磁盘I / O bound,同时运行的快速哈希函数可能比顺序读取更快,因此不会添加到整个过程。这可能是一种可能性(特别是如果有效地检测每个记录的开始/结束等顺序读取),这就是为什么我通过编写“取决于节省的时间来提升我的赌注” ...“。这表示磁盘上记录的实际结构是问题的开放参数之一(例如,如果我们只是从目录中的单个文件读取,因此强制执行非顺序读取),并且可能还有TeraByte大小的存储由花哨的RAID支持,其中寻求延迟同时保持关注通常得到很大改善。 我坚持我的建议,两个通道的方法 可能 比每个记录完全散列的方式更有效,但我希望我强调了这种可能性,单通道方法的好处。与许多面试问题一样,手头的情况有几个特点没有说明;这个想法并不是要让申请人提供绝对正确的答案(尽管有些答案可能是错误的!)而是要深入了解他/她的思维过程以及识别选项和决策点的能力。

答案 3 :(得分:6)

找到合适的散列函数并散列每条记录,将带有索引的散列列表存储到文件中。现在按哈希对哈希文件进行排序。最后检查匹配哈希的整个记录​​是否真实重复。

当然,这取决于您希望找到多少重复项以及之后您将如何处理这些信息。

答案 4 :(得分:3)

一次将数据加载到512M内存中,然后对该块进行排序并将其写入磁盘(作为自己的文件)。一旦整个1T以这种方式完成,将各个文件合并 - 排序成一个大的honkin'文件,然后按顺序读取该大(已排序)文件,将其写入最终文件,同时删除重复记录。

1T,每次512M,将是大约210万个文件(假设SI单位的二进制定义而不是十进制)。 512M的1K记录一次只允许内存中有524,288条记录,因此您可能需要分两个阶段进行合并排序。换句话说,对四组中的210万个文件进行合并排序以创建四个更大的文件,然后将这四个文件合并排序到大的排序文件中。然后,这是您按顺序处理以删除重复项的那个。

合并排序只是简单地合并多个已经排序的文件,只需从每个文件中选择第一个剩余记录并选择“最低”。例如,两个文件ab

a   b
7   6
3   5
1   4
    2
 \_/
  1 (a)
  2 (b)
  3 (a)
  4 (b)
  5 (b)
  6 (b)
  7 (a)

答案 5 :(得分:1)

生成每条记录的哈希值;记录内存中的记录号和哈希值,必要时填写文件(在写入文件之前按哈希顺序对数据进行排序)。当你想出一个新的哈希时,检查它是否已经存在于内存中 - 这是一个早期检测。 (这可能是也可能不是主要的好处。)。

当您阅读完所有数据后,您将拥有几个哈希值和记录号的文件,这些文件已经排序。将这些文件合并在一起,随时查看重复项。您甚至不需要记录此应用程序的重复项,因此一旦它们被证明是唯一的,您就可以丢弃哈希值。

考虑到大小 - 0.5 GB内存,1000 GB数据,每条记录1 KB,所以大约10亿条记录 - 假设256位散列(尽管128位可能就足够了),我们将使用32字节对于散列和4个字节的记录号,以及大约10亿条记录,我们需要大约36 GB的排序文件,在500 MB文件中生成(对应于可用的内存),因此有70-80个文件最后合并,看起来很容易管理。该列表将为您提供记录号 - 您必须访问1 TB文件才能读取记录。你需要考虑一下你要对复制品做些什么;您是否需要有关初始记录附加信息的信息,以及您保留的重复项和拒绝的重要项。等

答案 6 :(得分:1)

首先,在无限大的驱动器上配置一个无限大的交换文件......

答案 7 :(得分:1)

您可以使用哈希来减少问题的大小。例如,如果您有1 TB数据,则定义散列函数,数据分为10个文件(每个文件的大小小于1 TB)。之后,如果一个文件仍然太大,请重复该过程,直到文件可以存储在内存中。最后,您可以按排序计算出现的时间。