Самый эффективный способ увеличить значение Map в Java - PullRequest
324 голосов
/ 17 сентября 2008

Надеюсь, этот вопрос не считается слишком базовым для этого форума, но посмотрим. Я задаюсь вопросом, как реорганизовать некоторый код для повышения производительности, который запускается несколько раз.

Скажем, я создаю список частот слов, используя карту (возможно, HashMap), где каждый ключ представляет собой строку с подсчитываемым словом, а значение представляет собой целое число, которое увеличивается каждый раз, когда токен слова найдено.

В Perl увеличение такого значения было бы тривиально легко:

$map{$word}++;

Но в Java все гораздо сложнее. Вот как я сейчас это делаю:

int count = map.containsKey(word) ? map.get(word) : 0;
map.put(word, count + 1);

Что, конечно, зависит от функции автобокса в новых версиях Java. Интересно, можете ли вы предложить более эффективный способ увеличения такой стоимости? Существуют ли даже хорошие причины для отказа от использования среды Collections и использования чего-то еще?

Обновление: я проверил несколько ответов. Смотри ниже.

Ответы [ 27 ]

342 голосов
/ 20 сентября 2008

Некоторые результаты теста

Я получил много хороших ответов на этот вопрос - спасибо, ребята - поэтому я решил провести несколько тестов и выяснить, какой метод на самом деле самый быстрый. Вот пять методов, которые я протестировал:

  • метод "ContainsKey", который я представил в вопрос
  • метод "TestForNull", предложенный Александром Димитровым
  • метод "AtomicLong", предложенный Хэнком Гаем
  • метод "Trove", предложенный Джрудольфом
  • метод "MutableInt", предложенный phax.myopenid.com

Метод

Вот что я сделал ...

  1. создал пять классов, которые были идентичны, за исключением различий, показанных ниже. Каждый класс должен был выполнить операцию, типичную для сценария, который я представил: открыть файл 10 МБ и прочитать его, затем выполнить подсчет частоты всех жетонов слов в файле. Так как это заняло в среднем всего 3 секунды, мне пришлось выполнять подсчет частоты (не I / O) 10 раз.
  2. рассчитал цикл из 10 итераций, но не , а не операцию ввода-вывода и записал общее время, затраченное (в секундах), по существу, используя метод Яна Дарвина в книге рецептов Java .
  3. выполнил все пять тестов подряд, а затем сделал это еще три раза.
  4. усреднил четыре результата для каждого метода.

Результаты

Сначала я представлю результаты и приведенный ниже код для тех, кому это интересно.

Метод ContainsKey оказался, как и ожидалось, самым медленным, поэтому я приведу скорость каждого метода по сравнению со скоростью этого метода.

  • ContainsKey: 30,654 секунды (базовый уровень)
  • AtomicLong: 29,780 секунд (в 1,03 раза быстрее)
  • TestForNull: 28,804 секунды (в 1,06 раза быстрее)
  • Скорость: 26,313 секунд (в 1,16 раза быстрее)
  • MutableInt: 25,747 секунд (в 1,19 раза быстрее)

Выводы

Может показаться, что только метод MutableInt и метод Trove значительно быстрее, поскольку только они дают прирост производительности более чем на 10%. Однако, если многопоточность является проблемой, AtomicLong может быть более привлекательным, чем другие (я не совсем уверен). Я также запустил TestForNull с final переменными, но разница была незначительной.

Обратите внимание, что я не профилировал использование памяти в различных сценариях. Я был бы рад услышать от любого, кто имеет хорошее представление о том, как методы MutableInt и Trove могут повлиять на использование памяти.

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

код

Вот ключевой код каждого метода.

ContainsKey

import java.util.HashMap;
import java.util.Map;
...
Map<String, Integer> freq = new HashMap<String, Integer>();
...
int count = freq.containsKey(word) ? freq.get(word) : 0;
freq.put(word, count + 1);

TestForNull

import java.util.HashMap;
import java.util.Map;
...
Map<String, Integer> freq = new HashMap<String, Integer>();
...
Integer count = freq.get(word);
if (count == null) {
    freq.put(word, 1);
}
else {
    freq.put(word, count + 1);
}

AtomicLong

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicLong;
...
final ConcurrentMap<String, AtomicLong> map = 
    new ConcurrentHashMap<String, AtomicLong>();
...
map.putIfAbsent(word, new AtomicLong(0));
map.get(word).incrementAndGet();

Trove

import gnu.trove.TObjectIntHashMap;
...
TObjectIntHashMap<String> freq = new TObjectIntHashMap<String>();
...
freq.adjustOrPutValue(word, 1, 1);

MutableInt

import java.util.HashMap;
import java.util.Map;
...
class MutableInt {
  int value = 1; // note that we start at 1 since we're counting
  public void increment () { ++value;      }
  public int  get ()       { return value; }
}
...
Map<String, MutableInt> freq = new HashMap<String, MutableInt>();
...
MutableInt count = freq.get(word);
if (count == null) {
    freq.put(word, new MutableInt());
}
else {
    count.increment();
}
174 голосов
/ 07 марта 2017

ОК, может быть старый вопрос, но с Java 8 есть более короткий путь:

Map.merge(key, 1, Integer::sum)

Что делает: если ключ не существует, укажите 1 в качестве значения, в противном случае сумма 1 - значение, связанное с ключ . Больше информации здесь

42 голосов
/ 18 августа 2014

Небольшое исследование в 2016 году: https://github.com/leventov/java-word-count, исходный код теста

Лучшие результаты по методу (чем меньше, тем лучше):

                 time, ms
kolobokeCompile  18.8
koloboke         19.8
trove            20.8
fastutil         22.7
mutableInt       24.3
atomicInteger    25.3
eclipse          26.9
hashMap          28.0
hppc             33.6
hppcRt           36.5

Время \ пространство результатов: image

32 голосов
/ 04 сентября 2012

Google Гуава твой друг ...

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

* 1013 Е.Г. *

AtomicLongMap<String> map = AtomicLongMap.create();
[...]
map.getAndIncrement(word);

Также возможно добавить более 1 к значению:

map.getAndAdd(word, 112L); 
31 голосов
/ 17 сентября 2008

@ Хэнк Гей

Как продолжение моего собственного (довольно бесполезного) комментария: Троув выглядит как путь. Если по какой-либо причине вы хотите придерживаться стандартного JDK, ConcurrentMap и AtomicLong может сделать код крошечным битным, хотя YMMV.

    final ConcurrentMap<String, AtomicLong> map = new ConcurrentHashMap<String, AtomicLong>();
    map.putIfAbsent("foo", new AtomicLong(0));
    map.get("foo").incrementAndGet();

оставит 1 в качестве значения на карте для foo. Реально, этот подход должен рекомендовать повышенную дружелюбность к многопоточности.

25 голосов
/ 17 сентября 2008

Это всегда хорошая идея, посмотреть в Библиотеке Google для такого рода вещей. В этом случае Multiset сделает свое дело:

Multiset bag = Multisets.newHashMultiset();
String word = "foo";
bag.add(word);
bag.add(word);
System.out.println(bag.count(word)); // Prints 2

Существуют методы, подобные Map, для перебора ключей / записей и т. Д. Внутренняя реализация в настоящее время использует HashMap<E, AtomicInteger>, поэтому вы не будете нести расходы на бокс.

21 голосов
/ 17 сентября 2008

Вы должны знать, что ваша первоначальная попытка

int count = map.containsKey(word) ? map.get(word) : 0;

содержит две потенциально дорогостоящие операции на карте, а именно containsKey и get. Первый выполняет операцию, потенциально очень похожую на последнюю, поэтому вы выполняете ту же самую работу дважды !

Если вы посмотрите на API для Map, get операции обычно возвращают null, когда карта не содержит запрошенный элемент.

Обратите внимание, что это даст решение, подобное

map.put( key, map.get(key) + 1 );

опасно, поскольку может дать NullPointerException с. Вы должны проверить на null сначала.

Также обратите внимание , и это очень важно, что HashMap s может содержать nulls по определению. Так что не каждый возвращенный null говорит, что "такого элемента нет". В этом отношении containsKey ведет себя иначе по сравнению с get, фактически сообщая вам , существует ли такой элемент. Обратитесь к API для деталей.

Однако в вашем случае вы можете не захотеть различать сохраненный null и «noSuchElement». Если вы не хотите разрешать null s, вы можете предпочесть Hashtable. Использование библиотеки-оболочки, как уже предлагалось в других ответах, может быть лучшим решением для ручной обработки, в зависимости от сложности вашего приложения.

Чтобы завершить ответ (и я забыл вставить его сначала, благодаря функции редактирования!), Лучший способ сделать это изначально - это get в final переменную, проверить null и put это обратно с 1. Переменная должна быть final, потому что она в любом случае неизменна. Компилятору может не понадобиться эта подсказка, но он понятнее.

final HashMap map = generateRandomHashMap();
final Object key = fetchSomeKey();
final Integer i = map.get(key);
if (i != null) {
    map.put(i + 1);
} else {
    // do something
}

Если вы не хотите полагаться на автобокс, вы должны сказать что-то вроде map.put(new Integer(1 + i.getValue()));.

20 голосов
/ 14 ноября 2015
Map<String, Integer> map = new HashMap<>();
String key = "a random key";
int count = map.getOrDefault(key, 0);
map.put(key, count + 1);

И вот как вы увеличиваете значение простым кодом.

Преимущества:

  • Не создается другой класс для изменяемого целого
  • Короткий код
  • Легко понять
  • Нет исключения нулевого указателя

Другой способ - использовать метод слияния, но это слишком много для простого увеличения значения.

map.merge(key, 1, (a,b) -> a+b);

Предложение: в большинстве случаев вам следует заботиться о читабельности кода, а не о небольшом выигрыше в производительности.

18 голосов
/ 17 сентября 2008

Другим способом было бы создание изменяемого целого числа:

class MutableInt {
  int value = 0;
  public void inc () { ++value; }
  public int get () { return value; }
}
...
Map<String,MutableInt> map = new HashMap<String,MutableInt> ();
MutableInt value = map.get (key);
if (value == null) {
  value = new MutableInt ();
  map.put (key, value);
} else {
  value.inc ();
}

конечно, это подразумевает создание дополнительного объекта, но издержки по сравнению с созданием Integer (даже с Integer.valueOf) не должны быть такими большими.

9 голосов
/ 25 мая 2016

Вы можете использовать метод computeIfAbsent в интерфейсе Map, предоставленном в Java 8 .

final Map<String,AtomicLong> map = new ConcurrentHashMap<>();
map.computeIfAbsent("A", k->new AtomicLong(0)).incrementAndGet();
map.computeIfAbsent("B", k->new AtomicLong(0)).incrementAndGet();
map.computeIfAbsent("A", k->new AtomicLong(0)).incrementAndGet(); //[A=2, B=1]

Метод computeIfAbsent проверяет, связан ли указанный ключ со значением или нет? Если связанного значения нет, то оно пытается вычислить свое значение, используя данную функцию отображения. В любом случае он возвращает текущее (существующее или вычисленное) значение, связанное с указанным ключом, или ноль, если вычисленное значение равно нулю.

Если вы столкнулись с ситуацией, когда несколько потоков обновляют общую сумму, вы можете взглянуть на LongAdder class. Из-за высокой конкуренции ожидаемая пропускная способность этого класса значительно выше, чем AtomicLong , за счет более высокого потребления пространства.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...