我正在寻找一种方法来重新调整大量不适合内存的数据(大约40GB)。
我有大约3000万条可变长度的条目存储在一个大文件中。我知道该文件中每个条目的起始位置和结束位置。我需要改组那些不适合RAM的数据。
我想到的唯一解决方案是将包含从1
到N
的数字的数组混洗,其中N
是条目数,Fisher-Yates algorithm和然后根据此顺序复制新文件中的条目。不幸的是,这个解决方案涉及大量的搜索操作,因此会非常慢。
是否有更好的解决方案来均匀分布大量数据?
答案 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 / 2
。 N
项目以两个M
- 项目序列标记为0
和1
,以保证按随机顺序提供给我们。合并它们的过程产生一系列N
项,使得每个项来自序列0
或序列1
,并且来自每个序列的相同数量的项目。它看起来像这样:
0: a b c d
1: w x y z
N: a w x b y c d z
请注意,虽然0
和1
中的项目似乎是有序的,但它们只是标签,而且顺序并不意味着什么。它仅用于将0
和1
的顺序与N
的顺序相关联。
由于我们可以从标签告诉每个项目来自哪个序列,我们可以创建一个"来源"零和一的序列。拨打c
。
c: 0 1 1 0 1 0 0 1
根据上面的定义,c
中的零值始终为零。
现在观察一下N
中任何给定的标签排序,我们可以直接重现c
序列,因为标签保留了它们来自的序列的信息。鉴于N
和c
,我们可以重现0
和1
序列。因此,我们知道总是有一条从序列N
返回到一个三元组(0, 1, c)
的路径。换句话说,我们有一个反向函数r
,它是从N
标签到三元组(0, 1, c)
- r(N) = (0, 1, c)
的所有排序集合中定义的。
我们还有来自任何三重f
的转发功能r(n)
,只需根据0
的值重新合并1
和c
。这两个函数一起表明r(N)
的输出与N
的排序之间存在一对一的对应关系。
但我们真正想要证明的是,这种一对一的对应关系是彻底的 - 也就是说,我们想要证明{{1}没有额外的排序不对应任何三元组,并且没有额外的三元组与N
的任何顺序不对应。如果我们可以证明这一点,那么我们可以通过以均匀随机的方式选择三元组N
,以均匀随机的方式选择N
的排序。
我们可以通过计算箱子来完成证明的最后部分。假设每个可能的三元组都有一个bin。然后,我们将(0, 1, c)
中每个N
的排序放入r(N)
给出的三元组中。如果订单的数量与订单数量完全相同,那么我们就会有详尽的一对一对应关系。
从组合学中,我们知道N
个唯一标签的排序数量为N!
。我们也知道0
和1
的排序数量均为M!
。我们知道可能的序列c
的数量为N choose M
,与N! / (M! * (N - M)!)
相同。
这意味着共有
M! * M! * N! / (M! * (N - M)!)
三倍。但是N = 2 * M
,N - M = M
,以及上面的内容会缩小为
M! * M! * N! / (M! * M!)
那只是N!
。 QED。
要以均匀随机的方式选择三元组,我们必须以均匀随机的方式选择三元组中的每个元素。对于0
和1
,我们在内存中使用简单的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
列表1
并N - 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)
中值的随机数生成算法需要生成从c
到0
的标签,以便最终输出对每个类别具有恰当数量的标签。 (换句话说,如果您将三个块与K - 1
,10
和12
项合并,则13
的最终值需要{ {1}}十次,c
十二次,0
十三次。)
我认为可能有一个1
时间,2
空间算法可以做到这一点,如果我能找到一个或工作一个,我会在这里发布。结果将是真正的O(N)
洗牌,就像Paul Hankin在回答结束时描述的那样。
答案 4 :(得分:1)
一种简单的方法是选择一个K
,使1/K
数据适合内存。假设你有16GB内存,你的数据可能是K=4
。我假设您的随机数函数的格式为rnd(n)
,可生成从0
到n-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)