外部随机播放:将大量数据从内存中移出

时间:2013-01-31 14:25:27

标签: java algorithm bigdata

我正在寻找一种方法来重新调整大量不适合内存的数据(大约40GB)。

我有大约3000万条可变长度的条目存储在一个大文件中。我知道该文件中每个条目的起始位置和结束位置。我需要改组那些不适合RAM的数据。

我想到的唯一解决方案是将包含从1N的数字的数组混洗,其中N是条目数,Fisher-Yates algorithm和然后根据此顺序复制新文件中的条目。不幸的是,这个解决方案涉及大量的搜索操作,因此会非常慢。

是否有更好的解决方案来均匀分布大量数据?

6 个答案:

答案 0 :(得分:5)

首先从您的脸上获取shuffle问题。通过为您的条目创建一个产生随机类似结果的哈希算法,然后对哈希进行正常的外部排序来做到这一点。

现在,您已将shuffle转换为sort您的问题转变为找到适合您的口袋和内存限制的有效外部排序算法。现在应该像google一样简单。

答案 1 :(得分:2)

我建议您保留一般方法,但在进行实际复制之前反转地图。这样,你按顺序阅读并进行分散的写入,而不是反过来。

在程序可以继续之前,必须在请求时进行读取。写入可以保留在缓冲区中,这增加了在实际执行写操作之前累积多次写入同一磁盘块的可能性。

答案 2 :(得分:2)

前提

根据我的理解,使用Fisher-Yates算法和关于条目位置的数据,您应该能够获得(并计算)以下列表:

struct Entry {
    long long sourceStartIndex;
    long long sourceEndIndex;
    long long destinationStartIndex;
    long long destinationEndIndex;
}

问题

从这一点开始,天真的解决方案是在源文件中查找每个条目,读取它,然后在目标文件中查找条目的新位置并写入。

这种方法的问题在于它使用的方式太多了。

解决方案

更好的方法是使用两个巨大的缓冲区为每个文件减少搜索次数。

我建议为源文件(比方说64MB)使用一个小缓冲区,为目标文件建议一个大缓冲区(用户可以负担得多 - 比如说2GB)。

最初,目标缓冲区将映射到目标文件的前2GB。此时,在源缓冲区中以64MB的块读取整个源文件。在阅读时,将正确的条目复制到目标缓冲区。到达文件末尾时,输出缓冲区应包含所有正确的数据。将其写入目标文件。

接下来,将输出缓冲区映射到目标文件的下一个2GB并重复该过程。继续,直到您编写完整个输出文件。

注意

由于条目具有任意大小,因此很可能在缓冲区的开头和结尾处有条目的后缀和前缀,因此您需要确保正确复制数据!

估计时间成本

执行时间主要取决于源文件的大小,应用程序的可用RAM以及HDD的读取速度。假设一个40GB的文件,2GB的RAM和200MB / s的HDD读取速度,该程序将需要读取800GB的数据(40GB *(40GB / 2GB))。假设硬盘驱动器没有高度分散,搜索所花费的时间可以忽略不计。这意味着读取将花费一个小时!但是,幸运的是,如果用户有8GB的RAM可用于您的应用程序,时间可能会减少到只有15到20分钟。

我希望这对你来说已经足够了,因为我没有看到任何其他更快的方法。

答案 3 :(得分:1)

虽然您可以按照OldCurmudgeon的建议对随机密钥使用外部排序,但随机密钥不是必需的。您可以在内存中混洗数据块,然后使用"随机合并来加入它们,"正如aldel所建议的那样。

值得指出"随机合并"意味着更清楚。给定两个大小相等的混洗序列,随机合并的行为与merge sort中的完全相同,但是要添加到合并列表的下一个项目是使用来自零和1的混洗序列的布尔值来选择的,与一个零一样多的零。 (在合并排序中,将使用比较进行选择。)

证明

我断言这种方法是不够的。我们怎么知道这个过程给出了一个混乱的序列,这样每个排序都是同样可能的?可以使用图表和一些计算来提供校样草图。

首先,定义。假设我们有 N个唯一商品,其中N是偶数,M = N / 2N项目以两个M - 项目序列标记为01,以保证按随机顺序提供给我们。合并它们的过程产生一系列N项,使得每个项来自序列0或序列1,并且来自每个序列的相同数量的项目。它看起来像这样:

0: a b c d
1: w x y z
N: a w x b y c d z

请注意,虽然01中的项目似乎是有序的,但它们只是标签,而且顺序并不意味着什么。它仅用于将01的顺序与N的顺序相关联。

由于我们可以从标签告诉每个项目来自哪个序列,我们可以创建一个"来源"零和一的序列。拨打c

c: 0 1 1 0 1 0 0 1

根据上面的定义,c中的零值始终为零。

现在观察一下N中任何给定的标签排序,我们可以直接重现c序列,因为标签保留了它们来自的序列的信息。鉴于Nc,我们可以重现01序列。因此,我们知道总是有一条从序列N返回到一个三元组(0, 1, c)的路径。换句话说,我们有一个反向函数r,它是从N标签到三元组(0, 1, c) - r(N) = (0, 1, c)的所有排序集合中定义的。

我们还有来自任何三重f的转发功能r(n),只需根据0的值重新合并1c。这两个函数一起表明r(N)的输出与N的排序之间存在一对一的对应关系。

但我们真正想要证明的是,这种一对一的对应关系是彻底的 - 也就是说,我们想要证明{{1}没有额外的排序不对应任何三元组,并且没有额外的三元组与N的任何顺序不对应。如果我们可以证明这一点,那么我们可以通过以均匀随机的方式选择三元组N,以均匀随机的方式选择N的排序。

我们可以通过计算箱子来完成证明的最后部分。假设每个可能的三元组都有一个bin。然后,我们将(0, 1, c)中每个N的排序放入r(N)给出的三元组中。如果订单的数量与订单数量完全相同,那么我们就会有详尽的一对一对应关系。

从组合学中,我们知道N个唯一标签的排序数量为N!。我们也知道01的排序数量均为M!。我们知道可能的序列c的数量为N choose M,与N! / (M! * (N - M)!)相同。

这意味着共有

M! * M! * N! / (M! * (N - M)!)

三倍。但是N = 2 * MN - M = M,以及上面的内容会缩小为

M! * M! * N! / (M! * M!)

那只是N!。 QED。

实施

要以均匀随机的方式选择三元组,我们必须以均匀随机的方式选择三元组中的每个元素。对于01,我们在内存中使用简单的Fisher-Yates shuffle来实现这一点。唯一剩下的障碍是生成一个正确的零和一系列序列。

非常重要 - 重要! - 仅生成相等数字的零和1的序列。否则,您没有从Choose(N, M)个序列中选择具有统一概率的,并且您的随机播放可能存在偏差。真正明显的做法是将包含相同数量的0和1的序列混洗......但问题的整个前提是我们不能在内存中容纳那么多的零和1!因此,我们需要一种方法来生成零和一些受约束的随机序列,这样就可以得到与1完全相同的零。

为了以概率相干的方式进行此操作,我们可以模拟从瓮中标记为零或一个的绘制球,而无需替换。假设我们从五十个0球和五十个1球开始。如果我们保持对每个球的数量的计数,我们可以保持选择一个或另一个的运行概率,以便最终结果没有偏差。 (可疑类似于Python的)伪代码将是这样的:

def generate_choices(N, M):
    n0 = M
    n1 = N - M
    while n0 + n1 > 0:
        if randrange(0, n0 + n1) < n0:
            yield 0
            n0 -= 1
        else:
            yield 1
            n1 -= 1

由于浮点错误,这可能不是完美,但它将非常接近完美。

算法的最后一部分至关重要。通过上述证据可以清楚地表明,其他生成零和零的方法不会给我们带来适当的洗牌。

在实际数据中执行多次合并

还有一些实际问题。上面的参数假设一个完美平衡的合并,它还假设您的数据只有内存的两倍。这两种假设都不可能成立。

结果证明这不是一个大问题,因为上述论点实际上并不需要同样大小的列表。只是如果列表大小不同,计算会稍微复杂一些。如果您通过以上内容替换M列表1N - M,则详细信息的排列方式相同。 (伪代码的编写方式适用于任何大于零且小于M的{​​{1}}。然后会有N个零和M个。{)

第二个意味着在实践中,可能有许多块以这种方式合并。该过程继承了合并排序的几个属性 - 特别是,它要求对于M - N块,您必须执行大约K个合并,然后K / 2合并,等等在所有数据合并之前。每批合并将循环遍历整个数据集,并且将大约K / 4个批次,运行时间为log2(K)。普通的Fisher-Yates shuffle在O(N * log(K))中是严格线性的,因此理论上对于非常大的N来说会更快。但是直到K变得非常非常大,惩罚可能比寻求惩罚的磁盘要小得多。

这种方法的好处来自智能IO管理。对于SSD来说,它甚至可能不值得 - 寻求惩罚可能不足以证明多次合并的开销。 Paul Hankin的答案有一些实用的技巧可以帮助我们思考所提出的实际问题。

一次合并所有数据

执行多个二进制合并的替代方法是一次合并所有块 - 这在理论上是可行的,并且可能导致K算法。 O(N)中值的随机数生成算法需要生成从c0的标签,以便最终输出对每个类别具有恰当数量的标签。 (换句话说,如果您将三个块与K - 11012项合并,则13的最终值需要{ {1}}十次,c十二次,0十三次。)

我认为可能有一个1时间,2空间算法可以做到这一点,如果我能找到一个或工作一个,我会在这里发布。结果将是真正的O(N)洗牌,就像Paul Hankin在回答结束时描述的那样。

答案 4 :(得分:1)

一种简单的方法是选择一个K,使1/K数据适合内存。假设你有16GB内存,你的数据可能是K=4。我假设您的随机数函数的格式为rnd(n),可生成从0n-1的统一随机数。

然后:

for i = 0 .. K-1
   Initialize your random number generator to a known state.
   Read through the input data, generating a random number rnd(K) for each item as you go.
   Retain items in memory whenever rnd(K) == i.
   After you've read the input file, shuffle the retained data in memory.
   Write the shuffled retained items to the output file.

这很容易实现,会避免很多寻求,而且显然是正确的。

另一种方法是根据随机数将输入数据划分为K个文件,然后遍历每个文件,在内存中进行混洗并写入磁盘。这减少了磁盘IO(每个项目被读取两次并写入两次,与第一种方法相比,每个项目被读取K次并写入一次),但是您需要小心缓冲IO以避免大量搜索,它使用更多的中间磁盘,实现起来有点困难。如果你只有40GB的数据(所以K很小),那么通过输入数据进行多次迭代的简单方法可能是最好的。

如果您使用20ms作为读取或写入1MB数据的时间(并且假设内存中的洗牌成本无关紧要),那么简单的方法将需要40 * 1024 *(K + 1)* 20ms,即1分8秒(假设为K=4)。假设您可以最小化搜索,中间文件方法将花费40 * 1024 * 4 * 20ms,大约55秒。请注意,SSD的读写速度大约快20倍(甚至忽略了搜索),因此您应该期望使用SSD在10秒内完成此任务。 Latency Numbers every Programmer should know

中的数字

答案 5 :(得分:-1)

  • 对数据库条目进行逻辑分区(例如按字母顺序排列)
  • 根据您创建的分区创建索引
  • 根据索引
  • 构建DAO以进行敏感化