11

我正在处理一个处理大量推文的项目;目标是在我处理重复项时删除它们。我有推文 ID,它以格式字符串的形式出现"166471306949304320"

我一直在使用HashSet<String>这个,它工作了一段时间。但是当我达到大约 1000 万个项目时,我彻底陷入困境并最终得到一个 GC 错误,大概来自重新散列。我尝试定义更好的尺寸/负载

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

这让它走得更远一点,但仍然非常缓慢(大约 1000 万,它需要 3 倍的时间来处理)。我该如何优化呢?鉴于我大概知道最后应该有多少项目(在这种情况下,大约 20-22 百万)我应该创建一个只重新散列两次或三次的 HashSet,或者这样的开销设置招致太多的时间惩罚?如果我不使用字符串,或者如果我定义不同的 HashCode 函数(在这种情况下是字符串的特定实例,我不知道该怎么做),事情会更好吗?这部分实现代码如下。

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; 
    }
4

3 回答 3

9

您可能希望超越 Java 集合框架。我已经进行了一些内存密集型处理,您将面临几个问题

  1. 大型哈希映射和哈希集的桶数将导致大量开销(内存)。您可以通过使用某种自定义哈希函数和模数来影响这一点,例如 50000
  2. 字符串在 Java 中使用 16 位字符表示。您可以通过对大多数脚本使用 utf-8 编码的字节数组来减半。
  3. HashMap 通常是非常浪费的数据结构,而 HashSet 基本上只是围绕这些结构的一个薄包装。

鉴于此,请查看 trove 或 guava 的替代品。此外,您的 id 看起来很长。这些是 64 位的,比字符串表示要小很多。

您可能要考虑的另一种选择是使用布隆过滤器(番石榴有一个不错的实现)。布隆过滤器会告诉您某些东西是否绝对不在集合中,并且如果包含某些东西,则具有合理的确定性(小于 100%)。结合一些基于磁盘的解决方案(例如数据库、mapdb、mecached...)应该可以很好地工作。您可以缓冲传入的新 id,分批写入它们,并使用布隆过滤器检查您是否需要在数据库中查找,从而在大多数情况下避免昂贵的查找。

于 2013-05-22T14:00:56.020 回答
2

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

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

我提供的链接很好地列出了这种方法的优缺点。

*顺便说一句,Jilles Van Gurp 建议的布隆过滤器是很棒的快速预过滤器。

于 2013-05-22T14:14:12.547 回答
0

简单、未经尝试且可能很愚蠢的建议:创建一个集合映射,由推文 ID 的第一个/最后 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));

这很容易让您将散列空间的最大大小保持在合理值以下。

于 2013-05-22T13:51:17.830 回答