我有一个非常大的不可变密钥集,它不适合内存,还有一个更大的引用列表,必须只扫描一次。如何在RAM中完成标记阶段?我确实有一个可能的解决方案,我将在稍后写一个答案(不想破坏它),但也许还有其他解决方案我没想过。
我会尝试重述问题,使其更“真实”:
你在Facebook工作,你的任务是找到哪些用户没有用表情符号创建帖子。您所拥有的只是活动用户名列表(大约20亿),以及您必须扫描的帖子列表(用户名/文本),但只需一次。它仅包含活动用户(您无需验证它们)。
此外,您有一台计算机,具有2 GB的RAM(1 GB的奖励积分)。因此必须在RAM中完成所有操作(无需外部排序或按排序顺序读取)。两天之内。
你能做到吗?怎么样?提示:您可能希望使用哈希表,以用户名作为键,并使用一位作为值。但是用户名列表不适合内存,因此不起作用。使用用户ID可能会有效,但您只需要名称。您可以扫描用户名列表几次(可能是40次,但不是更多次)。答案 0 :(得分:1)
听起来像是我10年前解决的一个问题。
第一阶段:沟渠GC。 GC对于小对象(几个字节)的开销可能超过100%。
第二阶段:为用户名设计一个合适的压缩方案。英文每个字符大约有3位。即使您允许更多字符,平均位数也不会快速上升。
第三阶段:在内存中创建用户名字典。使用每个用户名的16位前缀来选择正确的子字典。读入所有用户名,最初只用这个前缀对它们进行排序。然后依次对每个字典进行排序。 如问题中所述,为“使用过的表情符号”结果为每个用户名分配一个额外的位。
问题现在是I / O绑定,因为计算是令人尴尬的并行。最长的阶段将是阅读所有帖子(这将是许多TB)。
请注意,在此设置中,您没有使用像String
这样的花哨数据类型。字典是连续的内存块。
如果截止日期为两天,我会抛弃一些这种幻想。读取文本的I / O范围非常严重,以至于用户数据库的创建可能超过16 GB。是的,这将交换到磁盘。一次性的大不了。
答案 1 :(得分:1)
散列键,对哈希值进行排序,并以压缩形式存储已排序的哈希值。
TL; DR
我建议的算法可以被视为the solution for similar (simpler) problem的扩展。
虽然使用值h = 2 * number_of_keys似乎相当不错,但我们可以尝试改变它以优化空间要求。 (设置太高会降低压缩比,将其设置得太低会导致重复次数过多)。
这种方法不能保证结果:可以发明10个错误的哈希函数,以便每次传递时都复制每个键。但很有可能它会成功并且很可能需要大约1GB RAM(因为大多数压缩整数值在[1..8]范围内,因此每个键在压缩流中产生大约2..3位)。
为了精确地估计空间要求,我们可以使用(复杂的?)数学证明或算法的完整实现(也非常复杂)。但是为了获得粗略估计,我们可以使用步骤1..4的部分实现。在Ideone上查看。它使用名为FSE的ANS编码器的变体(取自此处:https://github.com/Cyan4973/FiniteStateEntropy)和简单的哈希函数实现(取自此处:https://gist.github.com/badboy/6267743)。结果如下:
Key list loads allowed: 10 20
Optimal h/n: 2.1 1.2
Bits per key: 2.98 2.62
Compressed MB: 710.851 625.096
Uncompressed MB: 40.474 3.325
Bitmap MB: 238.419 238.419
MB used: 989.744 866.839
Index entries: 1'122'520 5'149'840
Indexed fragment size: 1781.71 388.361
对于10个键扫描的原始OP限制,哈希范围的最佳值仅比我的猜测(2.0)略高(2.1),并且此参数非常方便,因为它允许使用32位哈希(而不是64位)那些)。所需的内存略小于1GB,这允许使用相当大的索引(因此步骤10将不会非常慢)。这里有一个小问题:这些结果显示了最后消耗了多少内存,但在这种特殊情况下(10次键扫描),我们在执行第二次传递时暂时需要超过1 GB的内存。如果我们删除第一个第一遍的结果(唯一哈希)并稍后重新计算它们,以及第7步,这可能会被修复。
由于20键扫描的限制不那么严格,哈希范围的最佳值为1.2,这意味着算法需要更少的内存并为索引提供更多空间(因此步骤10将快5倍)。
放宽对40次键扫描的限制不会导致任何进一步的改进。
答案 2 :(得分:1)
创建最小完美哈希函数(MPHF)。 每个密钥大约1.8位(使用 RecSplit algorithm),这大约使用了429 MB。 (这里,1 MB是2 ^ 20字节,1 GB是2 ^ 30字节。) 对于每个用户,分配一位作为标记,大约238 MB。 因此内存使用量约为667 MB。 然后阅读帖子,为每个用户计算哈希值, 并根据需要设置相关位。 再次读取用户表,计算散列,检查该位是否已设置。
生成MPHF有点棘手,不是因为它很慢 (这可能需要大约30分钟的CPU时间), 但由于内存使用情况。使用1 GB或RAM, 它需要分段完成。 我们假设我们使用32个大小相同的段,如下所示:
上述算法读取用户列表32次。 如果使用更多的段,这可以减少到大约10 (例如一百万), 并且每步读取许多段,以适应内存。 对于较小的段,每个键需要较少的位 减少一个段内重复的可能性。
答案 3 :(得分:0)
我能想到的最简单的解决方案是老式的批量更新程序。它需要几个步骤,但在概念上它并不比合并内存中的两个列表复杂。这是我们几十年前在银行数据处理方面所做的事情。
sort
实用程序或任何其他程序来轻松地执行此操作,该程序将对大于适合内存的文件进行排序。这是一般的想法:
currentUser = get first user name from users file
currentPost = get first post from database stream
usedEmoji = false
while (not at end of users file and not at end of database stream)
{
if currentUser == currentPostUser
{
if currentPost has emoji
{
usedEmoji = true
}
currentPost = get next post from database
}
else if currentUser > currentPostUser
{
// No user for this post. Get next post.
currentPost = get next post from database
usedEmoji = false
}
else
{
// Current user is less than post user name.
// So we have to switch users.
if (usedEmoji == false)
{
// No post by this user contained an emoji
output currentUser name
}
currentUser = get next user name from file
}
}
// at the end of one of the files.
// Clean up.
// if we reached the end of the posts, but there are still users left,
// then output each user name.
// The usedEmoji test is in there strictly for the first time through,
// because the current user when the above loop ended might have had
// a post with an emoji.
while not at end of user file
{
if (usedEmoji == false)
{
output currentUser name
}
currentUser = get next user name from file
usedEmoji = false
}
// at this point, names of all the users who haven't
// used an emoji in a post have been written to the output.
另一种实现方式是,如果获得#2中描述的帖子列表过于繁琐,那就是按照自然顺序扫描帖子列表,并从包含表情符号的任何帖子输出用户名。然后,对生成的文件进行排序并删除重复项。然后,您可以继续进行与上述类似的合并,但是您不必明确检查帖子是否有表情符号。基本上,如果两个文件中都出现了名称,那么您就不会输出它。