【问题标题】:Java: optimize hashset for large-scale duplicate detectionJava:优化哈希集以进行大规模重复检测
【发布时间】:2013-05-17 14:26:29
【问题描述】:

我正在处理一个处理大量推文的项目;目标是在我处理重复项时删除它们。我有推文 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&lt;String&gt; 非常庞大且不需要,因为String.hashCode() 对于这种规模来说太高了。接下来我尝试了一个 Trie,但它在超过 100 万个条目时崩溃了;重新分配数组是有问题的。我使用HashSet&lt;Long&gt; 来获得更好的效果,几乎成功了,但是速度下降了,最终在处理的最后一站(大约 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; 
    }

【问题讨论】:

  • 如何将 ID 视为数字,找到一个好的基值,然后处理差异?然后您可以使用HashSet&lt;Long&gt;,它的性能应该优于字符串;您还可以使用 Trove 库来处理原语。
  • 你不能简单地增加你的堆大小吗?
  • 如果知道集合最终会包含2200万个项目,为什么不从一开始就创建一个容量为22_000_000 / 0.75的HashSet呢?这样可以防止任何重新散列。
  • @JBNizet 你的意思是 22_000_000 / 1.0?
  • 至于使用java -Xms2gb 之类的东西来增加堆大小,我的理解是,这将是针对 GC 错误的创可贴,但对显着降低速度没有帮助。

标签: java optimization hashset duplicate-removal


【解决方案1】:

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

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

【讨论】:

  • 增加了很多操作......这基本上是一个哈希(+几个等于)的哈希,你不会得到任何东西
【解决方案2】:

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

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

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

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

【讨论】:

    【解决方案3】:

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

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

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

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

    【讨论】:

    • 我怎么没想到呢?我已经在程序的另一部分使用 Trie,但没有想到为这个问题制作一个。如果可行(现在看来很明显),您肯定会得到答案。
    • 哎哟。我的 GC 过载只有 100 万条记录。我认为 Trie 行不通。
    • 也许我执行错了?我的只是一个 10 字符的递归数组列表,用于字符 0-9 - '0'。我想增加一百万次会导致内存使用膨胀并要求重新分配。考虑到我所知道的输入是 0-9 位和 18 位长的数字,你知道更有效的实现吗?
    • 我猜每个 Trie 节点都有 1 个字符和一个子数组/列表。不理解 10 字符递归数组
    • 是的,我是如何实现它的。每个节点都有 children[10] 和 19 字符点的标签。
    猜你喜欢
    • 2017-02-15
    • 1970-01-01
    • 2013-05-30
    • 2011-11-23
    • 1970-01-01
    • 2013-12-29
    • 1970-01-01
    • 1970-01-01
    • 2017-12-23
    相关资源
    最近更新 更多