Java: оптимизация hashset для широкомасштабного обнаружения дубликатов

Я работаю над проектом, где я обрабатываю много твитов; цель заключается в удалении дубликатов при их обработке. У меня есть идентификаторы твитов, которые входят в виде строк формата "166471306949304320"

Я использовал для этого HashSet<String>, который отлично работает некоторое время. Но к тому времени, когда я доберусь до 10 миллионов предметов, я резко увяз, и в итоге получаю ошибку GC, по-видимому, от перефразирования. Я попытался определить лучший размер/нагрузку с помощью

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

и это позволяет ему немного дальше, но по-прежнему мучительно медленным (примерно на 10 миллионов это занимает в 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, но он разбился чуть более 1 миллиона записей; перераспределение массивов было проблематичным. Я использовал HashSet<Long> для лучшего эффекта и почти сделал это, но скорость заглохла и, наконец, разбилась на последнем этапе обработки (около 19 миллионов). Решение пришло с выходом из стандартной библиотеки и с помощью Trove. Он закончил 22 миллиона записей на несколько минут быстрее, чем вообще не проверял дубликаты. Окончательная реализация была простой и выглядела так:

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

Ответ 1

Возможно, вам захочется взглянуть за рамки рамки Java. Я сделал некоторую интенсивную обработку данных, и вы столкнетесь с несколькими проблемами.

  • Количество ведер для больших хэш-карт и хеш-наборов будет вызывают много накладных расходов (памяти). Вы можете повлиять на это, используя какой-то пользовательской хэш-функции и по модулю, например, 50000
  • Строки представлены с использованием 16-битных символов в Java. Вы можете вдвое уменьшить это с помощью массивов байтов, базирующихся на utf-8, для большинства скриптов.
  • HashMaps - это, в общем, довольно расточительные структуры данных, а HashSets - это всего лишь тонкая оболочка вокруг них.

Учитывая это, взгляните на trove или guava на альтернативы. Кроме того, ваши идентификаторы выглядят как длинные. Это 64 бит, что немного меньше строкового представления.

Альтернативой, которую вы, возможно, захотите рассмотреть, является использование фильтров цветения (у guava есть достойная реализация). Фильтр цветения скажет вам, если что-то определенно не находится в наборе и с разумной уверенностью (менее 100%), если что-то содержится. Это в сочетании с некоторым решением на основе диска (например, database, mapdb, mecached,...) должно работать достаточно хорошо. Вы можете буферизовать входящие новые идентификаторы, записывать их в партиях и использовать фильтр цветка, чтобы проверить, нужно ли вам искать в базе данных и тем самым избегать дорогостоящих поисков в большинстве случаев.

Ответ 2

Если вы просто ищете существование строк, тогда я предлагаю вам попробовать использовать Trie (также называемое префиксным деревом). Общее пространство, используемое Trie, должно быть меньше, чем HashSet, и это быстрее для поиска строк.

Основной недостаток заключается в том, что он может быть медленнее при использовании с жестким диском при загрузке дерева, а не в сохраненной линейной структуре, такой как хэш. Поэтому убедитесь, что он может находиться внутри ОЗУ.

Ссылка, которую я дал, является хорошим списком плюсов и минусов этого подхода.

* как в сторону, фильтры цветения, предложенные Джилсом Ван Гурпом, являются очень быстрыми префильтрами.

Ответ 3

Простые, неопытные и, возможно, глупые предложения: Создайте карту наборов, индексированных по первым/последним 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));

Это легко позволяет сохранить максимальный размер пространства хеширования ниже разумного значения.