Я надеюсь, что этот вопрос не считается слишком основным для этого форума, но посмотрим. Мне интересно, как реорганизовать некоторый код для повышения производительности, который запускается несколько раз.
Скажем, я создаю список частот слов, используя карту (возможно, HashMap), где каждый ключ представляет собой строку с подсчитываемым словом, а значение представляет собой целое число, которое увеличивается при каждом обнаружении токена слова.
В Perl увеличение такого значения было бы несложно:
$map{$word}++;
Но в Java все гораздо сложнее. Вот как я сейчас это делаю:
int count = map.containsKey(word) ? map.get(word) : 0;
map.put(word, count + 1);
Что, конечно, зависит от функции автобокса в новых версиях Java. Интересно, можете ли вы предложить более эффективный способ увеличения такой стоимости? Существуют ли даже хорошие причины производительности для отказа от среды Collections и использования чего-то еще?
Обновление: я проверил несколько ответов. См. ниже.
источник
Ответы:
Некоторые результаты теста
Я получил много хороших ответов на этот вопрос - спасибо, ребята - поэтому я решил провести несколько тестов и выяснить, какой метод на самом деле самый быстрый. Пять методов, которые я протестировал, таковы:
метод
Вот что я сделал ...
Результаты
Сначала я представлю результаты и код ниже для тех, кто заинтересован.
Метод ContainsKey , как и ожидалось, был самым медленным, поэтому я приведу скорость каждого метода по сравнению со скоростью этого метода.
Выводы
Похоже, что только метод MutableInt и метод Trove значительно быстрее, и только они дают повышение производительности более чем на 10%. Однако, если многопоточность является проблемой, AtomicLong может быть более привлекательным, чем другие (я не совсем уверен). Я также запустил TestForNull с
final
переменными, но разница была незначительной.Обратите внимание, что я не профилировал использование памяти в различных сценариях. Я был бы рад услышать от любого, кто имеет хорошее представление о том, как методы MutableInt и Trove могут повлиять на использование памяти.
Лично я считаю метод MutableInt наиболее привлекательным, поскольку он не требует загрузки сторонних классов. Так что, если я не обнаружу проблемы с этим, я, скорее всего, пойду.
Код
Вот ключевой код из каждого метода.
ContainsKey
TestForNull
AtomicLong
Trove
MutableInt
источник
HashMap
, а не aConcurrentHashMap
. Это также должно бытьAtomicInteger
, а неAtomicLong
, опять же для равного сравнения. --- Кроме того, anint[1]
будет простой встроенной версиейMutableInt
, не требующей нового класса.freq.compute(word, (key, count) -> count == null ? 1 : count + 1)
? Внутренне он делает поиск менее хешированным, чемcontainsKey
было бы интересно посмотреть, как он сравнивается с другими из-за лямбды.Теперь есть более короткий путь с использованием Java 8
Map::merge
.Что оно делает:
Больше информации здесь .
источник
map.merge(key, 1, (a, b) -> a + b);
сделалоInteger::sum
как BiFunction, и ему не нравится, когда @russter отвечает так, как было написано. Это сработало для меняMap.merge(key, 1, { a, b -> a + b})
Небольшое исследование в 2016 году: https://github.com/leventov/java-word-count , исходный код теста
Лучшие результаты по методу (чем меньше, тем лучше):
Время \ пространство результаты:
источник
Google Guava - твой друг ...
... по крайней мере, в некоторых случаях. У них есть этот хороший AtomicLongMap . Особенно приятно, потому что вы имеете дело с длинной ценностью на вашей карте.
Например
Также возможно добавить более 1 к значению:
источник
AtomicLongMap#getAndAdd
принимает примитив,long
а не класс-обертку; нет смысла делатьnew Long()
. ИAtomicLongMap
является параметризованным типом; Вы должны были объявить это какAtomicLongMap<String>
.@ Хэнк Гей
В качестве продолжения моего собственного (довольно бесполезного) комментария: Троув выглядит как путь. Если по каким - либо причинам, вы хотели придерживаться стандартного JDK, ConcurrentMap и AtomicLong может сделать код крошечные немного лучше, хотя YMMV.
оставит
1
в качестве значения в карте дляfoo
. На самом деле, повышенный уровень дружелюбия к потокам - это все, что этот подход должен рекомендовать.источник
И вот как вы увеличиваете значение с помощью простого кода.
Выгода:
Даунсайд:
Теоретически, когда вы вызываете get (), вы уже знаете, куда поместить (), поэтому вам не придется искать снова. Но поиск в хэш-карте обычно занимает очень мало времени, так что вы можете игнорировать эту проблему производительности.
Но если вы очень серьезно относитесь к проблеме, вы перфекционист, другой способ - использовать метод слияния, который (вероятно) более эффективен, чем предыдущий фрагмент кода, поскольку вы (теоретически) будете искать карту только один раз: (хотя этот код не очевиден с первого взгляда, он короткий и производительный)
Предложение: большую часть времени вы должны заботиться о читабельности кода, а не о небольшом выигрыше в производительности. Если вам проще понять первый фрагмент кода, используйте его. Но если вы в состоянии понять 2-й штраф, вы также можете пойти на это!
источник
Для такой вещи всегда полезно заглянуть в Библиотеку Google Collections . В этом случае Multiset сделает свое дело:
Существуют методы, подобные Map, для перебора ключей / записей и т. Д. Внутренняя реализация в настоящее время использует a
HashMap<E, AtomicInteger>
, поэтому вы не будете нести расходы на бокс.источник
count()
метод на мультимножестве за O (1) или O (n) время (наихудший случай)? Документы неясны по этому вопросу.Вы должны знать о том, что ваша первоначальная попытка
содержит две потенциально дорогие операции на карте, а именно
containsKey
иget
. Первый выполняет операцию, потенциально очень похожую на последнюю, поэтому вы выполняете одну и ту же работу дважды !Если вы посмотрите на API для Map,
get
операции обычно возвращаются,null
когда карта не содержит запрошенный элемент.Обратите внимание, что это сделает решение как
опасно, так как это может привести к
NullPointerException
с. Вы должны проверить дляnull
первого.Также обратите внимание , и это очень важно, что
HashMap
s может содержатьnulls
по определению. Так что не каждый вернувшийсяnull
говорит "нет такого элемента". В этом отношении,containsKey
ведет себя по- разному отget
в самом деле говорит вам ли есть такой элемент. Обратитесь к API для деталей.Однако в вашем случае вы можете не захотеть различать сохраненный
null
и noSuchElement. Если вы не хотите разрешатьnull
s, вы можете предпочестьHashtable
. Использование библиотеки-оболочки, как уже предлагалось в других ответах, может быть лучшим решением для ручной обработки, в зависимости от сложности вашего приложения.Для того, чтобы завершить ответ (и я забыл положить , что в на первом, благодаря функции редактирования!), Лучший способ сделать это изначально, чтобы
get
вfinal
переменной, проверьтеnull
иput
его обратно в с1
. Переменная должна бытьfinal
потому, что она в любом случае неизменна. Компилятору может не понадобиться эта подсказка, но она понятнее.Если вы не хотите полагаться на автобокс, вы должны сказать что-то вроде
map.put(new Integer(1 + i.getValue()));
этого.источник
Другим способом было бы создание изменяемого целого числа:
конечно, это подразумевает создание дополнительного объекта, но издержки по сравнению с созданием Integer (даже с Integer.valueOf) не должны быть такими большими.
источник
Вы можете использовать метод computeIfAbsent в
Map
интерфейсе, представленном в Java 8 .Метод
computeIfAbsent
проверяет, связан ли указанный ключ со значением или нет? Если связанного значения нет, то оно пытается вычислить свое значение, используя данную функцию отображения. В любом случае он возвращает текущее (существующее или вычисленное) значение, связанное с указанным ключом, или ноль, если вычисленное значение равно нулю.Кроме того, если у вас есть ситуация, когда несколько потоков обновляют общую сумму, вы можете взглянуть на класс LongAdder. Из-за высокой конкуренции ожидаемая пропускная способность этого класса значительно выше, чем
AtomicLong
за счет более высокого потребления пространства.источник
Вращение памяти может быть проблемой здесь, поскольку каждый бокс целого числа, большего или равного 128, вызывает выделение объекта (см. Integer.valueOf (int)). Хотя сборщик мусора очень эффективно обрабатывает недолговечные объекты, производительность в некоторой степени пострадает.
Если вы знаете, что количество сделанных приращений будет в значительной степени превосходить количество ключей (= слов в данном случае), рассмотрите возможность использования вместо этого держателя int Факс уже представил код для этого. Здесь снова, с двумя изменениями (класс держателя сделан статическим и начальное значение установлено в 1):
Если вам нужна предельная производительность, ищите реализацию Map, которая непосредственно ориентирована на примитивные типы значений. Джрудольф упомянул GNU Trove .
Кстати, хорошим поисковым термином для этой темы является «гистограмма».
источник
Вместо вызова функции hasKey () быстрее вызывать map.get и проверять, является ли возвращенное значение нулевым или нет.
источник
Вы уверены, что это узкое место? Вы сделали какой-нибудь анализ производительности?
Попробуйте использовать средство профилирования NetBeans (оно бесплатное и встроено в NB 6.1) для просмотра горячих точек.
Наконец, обновление JVM (скажем, с 1.5-> 1.6) часто является дешевым средством повышения производительности. Даже обновление номера сборки может обеспечить хорошее повышение производительности. Если вы работаете в Windows, и это приложение серверного класса, используйте -server в командной строке, чтобы использовать JVM Server Hotspot. На машинах Linux и Solaris это определяется автоматически.
источник
Есть несколько подходов:
Используйте сумку, как и наборы, содержащиеся в Google Collections.
Создайте изменяемый контейнер, который вы можете использовать на карте:
И используйте put («слово», new My («Слово»)); Затем вы можете проверить, существует ли он и увеличивается ли при добавлении.
Старайтесь не использовать собственное решение, используя списки, потому что, если вы получите внутренний цикл поиска и сортировки, ваша производительность будет вонять. Первое решение HashMap на самом деле довольно быстрое, но, скорее всего, такое решение, которое можно найти в Google Collections, лучше.
Подсчет слов с помощью Google Collections, выглядит примерно так:
Использование HashMultiset довольно удобно, потому что алгоритм сумок - это то, что вам нужно для подсчета слов.
источник
Я думаю, что ваше решение будет стандартным, но, как вы сами отметили, это, вероятно, не самый быстрый способ.
Вы можете посмотреть на GNU Trove . Это библиотека, которая содержит все виды быстрых примитивных коллекций. Ваш пример будет использовать TObjectIntHashMap, у которого есть метод AdjustOrPutValue, который делает именно то, что вы хотите.
источник
Вариант подхода MutableInt, который может быть даже более быстрым, если его взломать, заключается в использовании одноэлементного массива int:
Было бы интересно, если бы вы могли повторно запустить тесты производительности с этим вариантом. Это может быть самым быстрым.
Изменить: вышеупомянутый шаблон работал хорошо для меня, но в конце концов я перешел на использование коллекций Trove, чтобы уменьшить объем памяти на некоторых очень больших картах, которые я создавал - и в качестве бонуса это было также быстрее.
Одна действительно хорошая особенность заключается в том, что у
TObjectIntHashMap
класса есть единственныйadjustOrPutValue
вызов, который, в зависимости от того, есть ли уже значение в этом ключе, либо установит начальное значение, либо увеличит существующее значение. Это идеально подходит для увеличения:источник
Google Collections HashMultiset:
- довольно элегантный в использовании
- но потребляет процессор и память
Лучше всего было бы иметь такой метод
Entry<K,V> getOrPut(K);
(элегантный и недорогой)Такой метод будет вычислять хеш и индексировать только один раз, и тогда мы сможем сделать с записью то, что мы хотим (либо заменить, либо обновить значение).
Более элегантно:
- возьмите
HashSet<Entry>
- расширьте его, чтобы
get(K)
при необходимости поставить новую запись- запись может быть вашим собственным объектом.
->
(new MyHashSet()).get(k).increment();
источник
Все очень просто, просто используйте встроенную функцию в
Map.java
следующем порядкеисточник
++
... OMG, это так просто. @siegi++
не работает где-либо в этом выражении, потому что переменная необходима в качестве ее операнда, но есть только значения. Ваше добавление+ 1
работ, хотя. Теперь ваше решение такое же, как в ответе off99555s ."поставить" нужно "получить" (чтобы избежать дублирования ключа).
Так что прямо сделайте «put»,
и если было предыдущее значение, сделайте дополнение:
Если count начинается с 0, то добавьте 1: (или любые другие значения ...)
Примечание: этот код не является потокобезопасным. Используйте его, чтобы построить, а затем использовать карту, а не обновлять ее одновременно.
Оптимизация: в цикле сохраняйте старое значение, чтобы оно стало новым значением следующего цикла.
источник
Различные примитивные обертки, например,
Integer
являются неизменяемыми, поэтому на самом деле нет более краткого способа сделать то, что вы просите, если вы не можете сделать это с чем-то вроде AtomicLong . Я могу дать это за минуту и обновить. Кстати, Hashtable является частью коллекции коллекций .источник
Я бы использовал Ленивую Карту Коллекций Apache (чтобы инициализировать значения 0) и использовал MutableIntegers из Apache Lang в качестве значений на этой карте.
Самой большой ценой является то, что вам придется дважды искать карту в вашем методе. По моему вы должны сделать это только один раз. Просто получите значение (оно будет инициализировано, если оно отсутствует) и увеличьте его.
источник
В структуре данных функциональной библиотеки Java
TreeMap
естьupdate
метод в последней заголовке магистрали:Пример использования:
Эта программа печатает "2".
источник
@Vilmantas Baranauskas: Что касается этого ответа, я бы прокомментировал, если бы у меня были точки повторения, но у меня его нет. Я хотел бы отметить, что определенный здесь класс Counter НЕ является потокобезопасным, так как недостаточно просто синхронизировать inc () без синхронизации value (). Другие потоки, вызывающие value (), не гарантированно увидят значение, если с обновлением не было установлено отношение «происходит до».
источник
Я не знаю, насколько это эффективно, но приведенный ниже код также работает. Вам нужно определить a
BiFunction
в начале. Кроме того, вы можете сделать больше, чем просто увеличить с помощью этого метода.выход
источник
Если вы используете Eclipse Collections , вы можете использовать
HashBag
. Это будет наиболее эффективный подход с точки зрения использования памяти, а также он будет хорошо работать с точки зрения скорости выполнения.HashBag
поддерживаетсяMutableObjectIntMap
объектом, который хранит примитивные целые вместоCounter
объектов. Это уменьшает накладные расходы памяти и повышает скорость выполнения.HashBag
предоставляет API, который вам нужен, так как этоCollection
также позволяет запрашивать количество вхождений элемента.Вот пример из коллекции Eclipse Kata .
Примечание: я являюсь коммиттером для Eclipse Collections.
источник
Я предлагаю использовать Java 8 Map :: compute (). Рассматривается также случай, когда ключ не существует.
источник
mymap.merge(key, 1, Integer::sum)
?Поскольку многие люди ищут в Java темы для ответов на Groovy, вот как вы можете сделать это в Groovy:
источник
Простой и легкий способ в Java 8 заключается в следующем:
источник
Надеюсь, я правильно понимаю ваш вопрос, я прихожу на Java из Python, чтобы сопереживать вашей борьбе.
если у тебя есть
ты бы сделал
Надеюсь это поможет!
источник