Java:优化hashset以进行大规模重复检测

时间:2013-05-22 13:43:22

标签: java optimization hashset duplicate-removal

我正在处理一个我处理大量推文的项目;我的目标是在处理它们时删除重复项。我有推文ID,它们以"166471306949304320"

格式的字符串形式出现

我一直在使用HashSet<String>,这可以正常工作一段时间。但到了大约1000万件物品的时候,我却陷入了巨大的困境,并最终得到了一个GC错误,大概是从重新开始。我尝试使用

定义更好的大小/负载

tweetids = new HashSet<String>(220000,0.80F);

这让它变得更远,但仍然非常缓慢(大约1000万,它需要3倍的处理时间)。我该如何优化呢?鉴于我已经大致知道在结尾集合中应该有多少项目(在这种情况下,大约20-22百万)我应该创建一个只重复两次或三次的HashSet,或者这样的开销是多少?设置了太多的时间罚款?如果我没有使用String,或者我定义了一个不同的HashCode函数(在这种情况下是String的特定实例,我不知道该怎么做),事情会更好吗?这部分实现代码如下。

tweetids = new HashSet<String>(220000,0.80F); // in constructor
duplicates = 0;
...
// In loop: For(each tweet)
String twid = (String) tweet_twitter_data.get("id");
// Check that we have not processed this tweet already
if (!(tweetids.add(twid))){
    duplicates++;
    continue; 
}

感谢您的推荐,我解决了这个问题。问题是哈希表示所需的内存量;首先,HashSet<String>只是巨大而且不必要,因为String.hashCode()对于这种规模而言过高。接下来,我尝试了一个Trie,但它在100多万个条目中崩溃了;重新分配阵列是有问题的。我使用了HashSet<Long>来更好地发挥作用并且几乎成功了,但速度已经衰退,最终在处理的最后一段(大约1900万)崩溃了。解决方案来自标准库并使用Trove。它完成了2200万条记录,比不检查重复条件快几分钟。最终的实现很简单,看起来像这样:

import gnu.trove.set.hash.TLongHashSet;
...
    TLongHashSet tweetids; // class variable
... 
    tweetids = new TLongHashSet(23000000,0.80F); // in constructor
...
    // inside for(each record)
    String twid = (String) tweet_twitter_data.get("id");
    if (!(tweetids.add(Long.parseLong(twid)))) {
        duplicates++;
        continue; 
    }

3 个答案:

答案 0 :(得分:9)

您可能希望超越Java集合框架。我做了一些内存密集型处理,你将遇到几个问题

  1. 大型哈希映射和散列集的存储桶数量 导致很多开销(内存)。您可以通过使用来影响这一点 某种自定义散列函数和例如模数。 50000
  2. 字符串在Java中使用16位字符表示。您可以通过对大多数脚本使用utf-8编码的字节数组来减半。
  3. HashMaps通常是非常浪费的数据结构,HashSets基本上只是一个很薄的包装。
  4. 考虑到这一点,看看特洛伊或番石榴的替代品。此外,你的ID看起来像多头。那些是64位,比字符串表示小很多。

    您可能想要考虑的替代方法是使用bloom过滤器(番石榴有一个不错的实现)。如果包含某些内容,布隆过滤器会告诉您某些内容是否肯定不在集合中并且具有合理的确定性(小于100%)。结合一些基于磁盘的解决方案(例如数据库,mapdb,mecached,......)应该可以很好地工作。您可以缓冲传入的新ID,批量编写它们,并使用bloom过滤器检查是否需要查看数据库,从而避免在大多数情况下进行昂贵的查找。

答案 1 :(得分:2)

如果您只是在寻找字符串的存在,那么我建议您尝试使用Trie(也称为前缀树)。 Trie使用的总空间应小于HashSet,并且字符串查找更快。

主要的缺点是,当它从硬盘中使用时可能会更慢,因为它正在加载树,而不是像Hash那样存储的线性结构。因此,请确保它可以保存在RAM内。

我给出的链接是这种方法的优点/缺点。

*另外,Jilles Van Gurp建议的布隆过滤器是快速预滤器。

答案 2 :(得分:0)

简单,未经验证且可能是愚蠢的建议:创建一个集合映射,由推文ID的前N个字符或后N个字符编制索引:

Map<String, Set<String>> sets = new HashMap<String, Set<String>>();
String tweetId = "166471306949304320";
sets.put(tweetId.substr(0, 5), new HashSet<String>());
sets.get(tweetId.substr(0, 5)).add(tweetId);
assert(sets.containsKey(tweetId.substr(0, 5)) && sets.get(tweetId.substr(0, 5)).contains(tweetId));

这样可以轻松地将散列空间的最大大小保持在合理的值以下。