Наиболее эффективный способ увеличения значения карты в Java

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

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

в Perl приращение такого значения будет тривиально просто:

$map{$word}++;

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

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

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

Update: я сделал тест нескольких ответов. Видеть под.

25 ответов


результаты теста

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

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

метод

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

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

результаты

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

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

  • ContainsKey: 30.654 секунд (базовый уровень)
  • AtomicLong: 29.780 секунд (1.03 раза быстро)
  • TestForNull: 28.804 секунд (1.06 раза быстрее)
  • Trove: 26.313 секунд (в 1,16 раза быстрее)
  • MutableInt: 25.747 секунд (в 1,19 раза быстрее)

выводы

похоже, что только метод MutableInt и метод Trove значительно быстрее, поскольку только они дают повышение производительности более чем на 10%. Однако, если threading является вопрос, 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();
}

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

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

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


небольшое исследование в 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

результаты Time\space:


@Hank Gay

как продолжение моего собственного (довольно бесполезного) комментария: Trove выглядит как путь. Если по какой-то причине вы хотите придерживаться стандартного 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. Реалистично, увеличенное дружелюбие к продевать нитку все что этот подход должен порекомендовать он.


Google гуавы - твой друг...

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

Э. Г.

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

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

map.getAndAdd(word, new Long(112)); 

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

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

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


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

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

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

если вы посмотрите на API для карт, get операции, как правило, вернуть null, когда карта не содержит требуемый элемент.

обратите внимание, что это принимают решение, как

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

опасно, так как это может привести к NullPointerExceptions. Вы должны проверить на null первый.

также Примечание!--40-->, а это очень важно, что HashMaps можете содержат nulls по определению. Так что не каждый вернулся null говорит: "такого элемента нет". В этом отношении, containsKey поведение по-разному С get фактически говоря вам ли есть такой элемент. Подробности см. В API.

Для вашего случая, однако, вы можете не захотеть различать сохраненный null и "noSuchElement". Если вы не хотите разрешить nulls Вы можете предпочесть 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())); вместо.


другой путь-создание изменчивое целое:

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

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


Map<String, Integer> map = new HashMap<>();
String key = "a random key";
int count = map.getOrDefault(key, 0);
map.put(key, count + 1);

и именно так вы увеличиваете значение с помощью простого кода.

выгода:

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

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

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

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


вращение памяти может быть проблемой здесь, так как каждый бокс int больше или равен 128 вызывает выделение объекта (см. Integer.valueOf (int)). Хотя сборщик мусора очень эффективно справляется с недолговечными объектами, производительность в некоторой степени пострадает.

Если вы знаете, что количество сделанных приращений будет в значительной степени превышать количество ключей (=слов в этом случае), рассмотрите возможность использования держателя int. Phax уже представил код для этого. Вот оно опять же, с двумя изменениями (класс держателя сделал статическое и начальное значение равным 1):

static class MutableInt {
  int value = 1;
  void inc() { ++value; }
  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();
}

Если вам нужна экстремальная производительность, найдите реализацию карты, которая непосредственно адаптирована к примитивным типам значений. джрудольф упомянул GNU Trove.

кстати, хороший поисковый термин для этой темы - "гистограмма".


вместо вызова containsKey () быстрее просто вызвать map.получить и проверить, является ли возвращаемое значение null или нет.

    Integer count = map.get(word);
    if(count == null){
        count = 0;
    }
    map.put(word, count + 1);

вы можете использовать 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 проверяет, связан ли указанный ключ со значением или нет? Если нет значения, то она пытается вычислить его значение с помощью функции сопоставления. В любом случае он возвращает текущее (существующее или вычисленное) значение, связанное с указанным ключом, или null, если вычисленное значение равно null.

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


вы уверены, что это узкое место? Вы провели анализ производительности?

попробуйте использовать профилировщик NetBeans (бесплатный и встроенный в NB 6.1) для просмотра горячих точек.

наконец, обновление JVM (скажем, от 1.5->1.6) часто является дешевым усилителем производительности. Даже обновление номера сборки может обеспечить повышение производительности. Если вы работаете в Windows, и это приложение серверного класса, используйте-server в командной строке для использования JVM точки доступа сервера. На Linux и Solaris машины это автоматически определяется.


есть несколько подходов:

  1. используйте мешок alorithm, как наборы, содержащиеся в коллекциях Google.

  2. создайте изменяемый контейнер, который вы можете использовать на карте:


    class My{
        String word;
        int count;
    }

и используйте put ("word", new My ("Word")); затем вы можете проверить, существует ли он и увеличивается при добавлении.

избегайте сворачивания собственного решения с помощью списков, потому что если вы получаете поиск и сортировку innerloop, представление будет вонять. Первое решение HashMap на самом деле довольно быстро, но правильное, как это найдено в коллекциях Google, вероятно, лучше.

подсчет слов с помощью Google Collections выглядит примерно так:



    HashMultiset s = new HashMultiset();
    s.add("word");
    s.add("word");
    System.out.println(""+s.count("word") );


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


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

вы можете посмотреть GNU Trove. Это библиотека, которая содержит все виды быстрых примитивных коллекций. Ваш пример будет использовать TObjectIntHashMap который имеет метод adjustOrPutValue, который делает именно то, что вы хотите.


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

Map<String,int[]> map = new HashMap<String,int[]>();
...
int[] value = map.get(key);
if (value == null) 
  map.put(key, new int[]{1} );
else
  ++value[0];

было бы интересно, если бы вы могли повторно запустить тесты производительности С этом варианте. Это может быть самым быстрым.


Edit: вышеприведенный шаблон отлично работал для меня, но в конце концов я изменил использование коллекций Trove для уменьшения размера памяти в некоторых очень больших картах, которые я создавал, и в качестве бонуса это также было быстрее.

одна очень приятная особенность заключается в том, что TObjectIntHashMap класс имеет один adjustOrPutValue вызовите это, в зависимости от того, есть ли уже значение в этом ключе, либо поставит начальное значение, либо увеличит существующее значение. Это идеально подходит для увеличения:

TObjectIntHashMap<String> map = new TObjectIntHashMap<String>();
...
map.adjustOrPutValue(key, 1, 1);

Коллекции Google HashMultiset:
- довольно элегантный в использовании
- но потребляйте процессор и память

лучше бы иметь метод : Entry<K,V> getOrPut(K); (элегантный, и низкая стоимость)

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

более элегантно:
- возьми HashSet<Entry>
- расширьте его так, чтобы get(K) при необходимости поставьте новую запись
- Вступление это может быть твой собственный объект.
--> (new MyHashSet()).get(k).increment();


"положить" нужно "получить" (чтобы не дублировать ключ).
Так что прямо сделайте "put",
а если было Предыдущее значение, то сделайте сложение:

Map map = new HashMap ();

MutableInt newValue = new MutableInt (1); // default = inc
MutableInt oldValue = map.put (key, newValue);
if (oldValue != null) {
  newValue.add(oldValue); // old + inc
}

Если count начинается с 0, добавьте 1: (или любые другие значения...)

Map map = new HashMap ();

MutableInt newValue = new MutableInt (0); // default
MutableInt oldValue = map.put (key, newValue);
if (oldValue != null) {
  newValue.setValue(oldValue + 1); // old + inc
}

обратите внимание : этот код не является потокобезопасным. Используйте его для сборки, а затем используйте карту, а не для одновременного ее обновления.

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

Map map = new HashMap ();
final int defaut = 0;
final int inc = 1;

MutableInt oldValue = new MutableInt (default);
while(true) {
  MutableInt newValue = oldValue;

  oldValue = map.put (key, newValue); // insert or...
  if (oldValue != null) {
    newValue.setValue(oldValue + inc); // ...update

    oldValue.setValue(default); // reuse
  } else
    oldValue = new MutableInt (default); // renew
  }
}

различные примитивные обертки, например,Integer неизменяемы, поэтому на самом деле нет более краткого способа сделать то, что вы просите Если вы можете сделать это с чем-то вроде AtomicLong. Я могу сделать это через минуту и обновить. Кстати,Hashtable и часть Основа Коллекции.


Я бы использовал ленивую карту Apache Collections (для инициализации значений до 0) и использовал MutableIntegers из Apache Lang в качестве значений на этой карте.

самая большая цена должна serach карта дважды в вашем методе. В моей ты должен сделать это только один раз. Просто получите значение (оно будет инициализировано, если отсутствует) и увеличьте его.


на Функциональная Java библиотеки TreeMap datastructure имеет update метод в последней голове ствола:

public TreeMap<K, V> update(final K k, final F<V, V> f)

пример использования:

import static fj.data.TreeMap.empty;
import static fj.function.Integers.add;
import static fj.pre.Ord.stringOrd;
import fj.data.TreeMap;

public class TreeMap_Update
  {public static void main(String[] a)
    {TreeMap<String, Integer> map = empty(stringOrd);
     map = map.set("foo", 1);
     map = map.update("foo", add.f(1));
     System.out.println(map.get("foo").some());}}

эта программа печатает "2".


@Vilmantas Baranauskas: что касается этого ответа, я бы прокомментировал, если бы у меня были очки репутации, но я этого не делаю. Я хотел отметить, что класс счетчика, определенный там, не является потокобезопасным, поскольку недостаточно просто синхронизировать inc () без синхронизации value (). Другие потоки, вызывающие value (), не гарантируют, что увидят значение, если не будет установлено отношение happens-before с обновлением.


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

public static Map<String, Integer> strInt = new HashMap<String, Integer>();

public static void main(String[] args) {
    BiFunction<Integer, Integer, Integer> bi = (x,y) -> {
        if(x == null)
            return y;
        return x+y;
    };
    strInt.put("abc", 0);


    strInt.merge("abc", 1, bi);
    strInt.merge("abc", 1, bi);
    strInt.merge("abc", 1, bi);
    strInt.merge("abcd", 1, bi);

    System.out.println(strInt.get("abc"));
    System.out.println(strInt.get("abcd"));
}

выход

3
1

если вы используете Коллекции Eclipse, вы можете использовать HashBag. Это будет наиболее эффективный подход с точки зрения использования памяти, а также будет хорошо работать с точки зрения скорости выполнения.

HashBag стоит MutableObjectIntMap который хранит примитивные ints вместо Counter объекты. Это уменьшает накладные расходы памяти и улучшает скорость выполнения.

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

вот пример Затмение Коллекции Ката.

MutableBag<String> bag =
  HashBag.newBagWith("one", "two", "two", "three", "three", "three");

Assert.assertEquals(3, bag.occurrencesOf("three"));

bag.add("one");
Assert.assertEquals(2, bag.occurrencesOf("one"));

bag.addOccurrences("one", 4);
Assert.assertEquals(6, bag.occurrencesOf("one"));

Примечание: я коммиттер для коллекций Eclipse.


поскольку многие люди ищут темы Java для заводных ответов, вот как вы можете сделать это в Groovy:

dev map = new HashMap<String, Integer>()
map.put("key1", 3)

map.merge("key1", 1) {a, b -> a + b}
map.merge("key2", 1) {a, b -> a + b}