Frage

Ich hoffe, dass diese Frage für dieses Forum nicht als zu grundlegend angesehen wird, aber wir werden sehen.Ich frage mich, wie ich Code umgestalten kann, der mehrmals ausgeführt wird, um eine bessere Leistung zu erzielen.

Angenommen, ich erstelle eine Worthäufigkeitsliste mithilfe einer Map (wahrscheinlich einer HashMap), wobei jeder Schlüssel ein String mit dem Wort ist, das gezählt wird, und der Wert eine Ganzzahl ist, die jedes Mal erhöht wird, wenn ein Token des Wortes gefunden wird.

In Perl wäre es trivial einfach, einen solchen Wert zu erhöhen:

$map{$word}++;

Aber in Java ist es viel komplizierter.Hier, wie ich es derzeit mache:

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

Was natürlich auf der Autoboxing-Funktion in den neueren Java-Versionen beruht.Ich frage mich, ob Sie einen effizienteren Weg vorschlagen können, einen solchen Wert zu erhöhen.Gibt es überhaupt gute Leistungsgründe dafür, auf das Collections-Framework zu verzichten und stattdessen etwas anderes zu verwenden?

Aktualisieren:Ich habe mehrere Antworten getestet.Siehe unten.

War es hilfreich?

Lösung

Einige Testergebnisse

Ich habe viele gute Antworten auf diese Frage bekommen - dank Leute - so habe ich beschlossen, einige Tests und herauszufinden, zu laufen, welche Methode tatsächlich schnellsten ist. Die fünf Methoden, die ich getestet, sind diese:

  • die "ContainsKey" Methode, die ich vorgestellt in die Frage
  • die "TestForNull" Methode vorgeschlagen von Aleksandar Dimitrov
  • die "Atomic" Methode vorgeschlagen von Hank Homosexuell
  • der "Trove" -Methode von jrudolph vorgeschlagen
  • die "MutableInt" Methode vorgeschlagen von phax.myopenid.com

Methode

Hier ist, was ich getan habe ...

  1. erstellt fünf Klassen, die mit Ausnahme der Unterschiede unten gezeigten identisch waren. Jede Klasse hatte eine Operation auszuführen, die typisch für das Szenario I dargestellt: eine 10 MB-Datei öffnen und sie in lesen, dann eine Frequenzzählung alle Wort-Token in der Datei ausgeführt wird. Da dies im Durchschnitt nur 3 Sekunden dauerte, ich hatte es die Frequenzzahl durchführt (nicht die I / O) 10-mal.
  2. timed die Schleife von 10 Iterationen aber nicht die I / O-Operation und aufgezeichnet, um die Gesamtzeit (in Sekunden Uhr) im wesentlichen unter Verwendung von Ian Darwin-Methode in dem Java-Kochbuch .
  3. ausgeführt alle fünf Tests in Serie, und dann tat dies noch dreimal.
  4. gemittelt, um die vier Ergebnisse für jede Methode.

Ergebnisse

Ich werde die Ergebnisse präsentiert ersten und der folgende Code für diejenigen, die interessiert sind.

Die ContainsKey Methode war, wie erwartet, die langsamste, so werde ich die Geschwindigkeit jedes Verfahren im Vergleich zu der Geschwindigkeit dieser Methode geben.

  • ContainsKey: 30,654 Sekunden (Baseline)
  • Atomic: 29,780 Sekunden (1,03-mal so schnell)
  • TestForNull: 28,804 Sekunden (1,06-mal so schnell)
  • Trove: 26,313 Sekunden (1,16-mal so schnell)
  • MutableInt: 25,747 Sekunden (1,19-mal so schnell)

Schlussfolgerungen

Es scheint, dass nur die MutableInt Verfahren und die Trove Verfahren deutlich schneller sind, dass sie nur eine Leistungssteigerung von mehr als 10% geben. Wenn jedoch Einfädeln ein Problem, könnte Atomic attraktiver sein als die andere (ich bin nicht wirklich sicher). Ich lief TestForNull auch mit final Variablen, aber der Unterschied war vernachlässigbar.

Beachten Sie, dass ich nicht die Speichernutzung in den verschiedenen Szenarien profiliert haben. Ich würde gerne von jemandem zu hören, die guten Erkenntnisse darüber, wie die MutableInt und Trove Methoden beeinflussen wären wahrscheinlich Speichernutzung hat.

Ich persönlich finde die MutableInt Verfahren des attraktivste, da es erfordert keine Drittanbieter-Klassen zu laden. Also, wenn ich Probleme mit ihm zu entdecken, das ist die Art, wie ich bin wahrscheinlich zu gehen.

Der Code

Hier ist der entscheidende Code von jedem Verfahren.

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

Atomic

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

Andere Tipps

OK, kann eine alte Frage, aber es gibt einen kürzeren Weg mit Java 8:

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

Was es tut: wenn Taste Sie existiert nicht, setzen Sie 1 als Wert, sonst Summe 1 auf den Wert im Zusammenhang mit Taste . Weitere Informationen

Ein wenig Recherche im Jahr 2016: https://github.com/leventov/java-word- zählen , Benchmark-Quellcode

Die besten Ergebnisse pro Methode (kleiner ist besser):

                 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

Zeit \ space Ergebnisse:

Google Guava ist dein Freund ...

... zumindest in einigen Fällen. Sie haben diese schöne AtomicLongMap . Besonders schön, weil Sie mit lange beschäftigen als Wert in Ihrer Karte.

z.

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

auch möglich, mehr als 1 auf den Wert hinzuzufügen:

map.getAndAdd(word, 112L); 

@Hank Homosexuell

Als Follow-up zu meinem eigenen (und nicht nutzlos) Kommentar: Trove sieht aus wie der Weg zu gehen. Wenn aus irgendeinem Grund Sie mit dem Standard-JDK, ConcurrentMap und Atomic können den Code machen eine winzige etwas schöner, obwohl YMMV.

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

wird 1 als Wert in der Karte für foo verlassen. Realistisch betrachtet, erhöhte Freundlichkeit Threading ist alles, was dieser Ansatz zu empfehlen hat.

Es ist immer eine gute Idee, betrachten die Google Sammlungen Bibliothek für diese Art die Sache. In diesem Fall wird ein Multiset wird es tun:

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

Es gibt Map ähnliche Methoden zur Iteration über Tasten / Einträge usw. Intern wird die Umsetzung derzeit verwendet eine HashMap<E, AtomicInteger>, so dass Sie nicht Boxen Kosten entstehen werden.

Sie sollten sich darüber im Klaren sein, dass Ihr ursprünglicher Versuch

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

enthält zwei potenziell teure Operationen auf einer Karte, nämlich containsKey Und get.Ersteres führt möglicherweise einen Vorgang aus, der dem letzteren ziemlich ähnlich ist, Sie erledigen also die gleiche Arbeit zweimal!

Wenn Sie sich die API für Map ansehen, get Operationen kehren normalerweise zurück null wenn die Karte das angeforderte Element nicht enthält.

Beachten Sie, dass dies zu einer Lösung wie folgt führt

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

gefährlich, da es nachgeben könnte NullPointerExceptionS.Sie sollten nach a suchen null Erste.

Beachten Sie auch, und das ist sehr wichtig, das HashMapS dürfen enthalten nulls per Definition.Es sind also nicht alle zurückgekehrt null sagt: „Es gibt kein solches Element“.Insofern, containsKey verhält anders aus get indem ich es dir tatsächlich sage ob Es gibt so ein Element.Weitere Informationen finden Sie in der API.

In Ihrem Fall möchten Sie jedoch möglicherweise nicht zwischen einem gespeicherten unterscheiden null und „noSuchElement“.Wenn Sie es nicht zulassen möchten nullVielleicht bevorzugen Sie a Hashtable.Abhängig von der Komplexität Ihrer Anwendung ist die Verwendung einer Wrapper-Bibliothek, wie bereits in anderen Antworten vorgeschlagen, möglicherweise eine bessere Lösung für die manuelle Behandlung.

Um die Antwort zu vervollständigen (und ich habe dank der Bearbeitungsfunktion zunächst vergessen, sie einzugeben!), ist es am besten, dies nativ zu tun get in ein final Variable, prüfen Sie auf null Und put es wieder rein mit einem 1.Die Variable sollte sein final weil es sowieso unveränderlich ist.Der Compiler benötigt diesen Hinweis möglicherweise nicht, aber so ist es klarer.

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

Wenn Sie sich nicht auf Autoboxing verlassen möchten, sollten Sie etwas sagen wie map.put(new Integer(1 + i.getValue())); stattdessen.

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

Und das ist, wie Sie einen Wert mit einfachen Code erhöhen.

Vorteil:

  • Nicht eine andere Klasse für wandelbar int Erstellen
  • Short Code
  • Leicht zu verstehen
  • Keine Null-Zeiger Ausnahme

Eine andere Möglichkeit ist merge Methode zu verwenden, aber das ist zu viel für nur einen Wert erhöht wird.

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

. Vorschlag: Sie sollten in den meisten Zeit über die Lesbarkeit des Codes mehr als wenig Leistungssteigerung sorgen

Eine andere Möglichkeit wäre eine veränderliche ganze Zahl erschaffen:

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

natürlich Dies impliziert eine zusätzliche Aufgabe zu schaffen, sondern den Aufwand im Vergleich zu einer ganzen Zahl zu schaffen (auch mit Integer.valueOf) sollte nicht so viel sein.

Sie können von computeIfAbsent Verfahren in Map Schnittstelle in 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]

Die Methode computeIfAbsent überprüft, ob der angegebene Schlüssel bereits mit einem Wert oder nicht zugeordnet ist? Wenn kein zugeordnete Wert wird dann versucht er seinen Wert mit der gegebenen Abbildungsfunktion zu berechnen. In jedem Fall gibt er den aktuellen (bestehenden oder berechneten) Wert mit dem angegebenen Schlüssel zugeordnet ist, oder null, wenn der berechnete Wert ist null.

Auf einer Seite zur Kenntnis, wenn Sie eine Situation, wo mehrere Threads eine gemeinsame Summe aktualisieren Sie einen Blick auf

Statt containsKey von () aufrufen, es ist schneller nur map.get anrufen und prüfen, ob der zurückgegebene Wert null ist oder nicht.

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

Sind Sie sicher, dass dies ein Engpass? Haben Sie eine Leistungsanalyse gemacht?

Versuchen Sie, die NetBeans Profiler (sein freies und gebaut in NB 6.1) an Hotspots zu suchen.

Schließlich wird ein JVM-Upgrade (sagt sich von 1.5-> 1.6) ist oft ein günstiger Performance-Booster. Auch kann ein Upgrade in Build-Nummer liefert gute Leistung steigert. Wenn Sie unter Windows ausgeführt werden und dies ist eine Serverklasse Anwendung verwenden -Server auf der Kommandozeile den Server Hotspot JVM zu verwenden. Unter Linux und Solaris-Maschinen wird diese automatisch erkannt.

Es gibt ein paar Ansätze:

  1. eine Tasche alorithm wie die Sets Verwendung in Google Sammlungen enthalten sind.

  2. Erstellen wandelbar Container, die Sie in der Karte verwenden können:


    class My{
        String word;
        int count;
    }

Und Verwendung put ( "Wort", neue My ( "Wort")); Dann können Sie prüfen, ob es existiert und Zuwachs beim Hinzufügen.

Vermeiden Sie Ihre eigene Lösung Rolllisten verwenden, denn wenn man Innenschleife Suche erhalten und Sortierung, wird Ihre Leistung stinken. Die erste HashMap Lösung ist eigentlich recht schnell, aber eine richtige wie in Google Sammlungen gefunden ist wahrscheinlich besser.

Counting Worte mit Google Sammlungen, etwa wie folgt aussehen:



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


die HashMultiset Verwendung ist ganz elegent, weil ein Beutel-Algorithmus ist genau das, was Sie brauchen, wenn Worte zu zählen.

Ich denke, Ihre Lösung der normale Weg sei, aber - wie Sie selbst bemerkt - es ist wahrscheinlich nicht der schnellste Weg möglich

.

Sie sehen können unter GNU Trove . Das ist eine Bibliothek, die alle Arten von schnellen primitiven Sammlungen enthalten. Ihr Beispiel würde verwenden, um eine TObjectIntHashMap , die eine Methode hat adjustOrPutValue, die genau das tut, was Sie wollen.

Eine Variation des MutableInt Ansatz, der noch schneller sein könnte, wenn ein bisschen wie ein Hack, eine Einzelelement-int-Array zu verwenden ist:

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

Es wäre interessant, wenn Sie Ihre Performance-Tests mit dieser Variante erneut ausführen können. Es könnte die schnellste sein.


Edit: Das obige Muster funktionierte gut für mich, aber schließlich änderte ich Trove die Sammlungen zu verwenden, die Speichergröße in einigen sehr großen Karten zu reduzieren Ich war die Schaffung - und als Bonus war es auch schneller

.

Ein wirklich nettes Feature ist, dass die TObjectIntHashMap Klasse einen einzigen adjustOrPutValue Anruf hat, dass, je nachdem, ob es bereits ein Wert zu diesem Schlüssel wird entweder einen Anfangswert setzen oder den vorhandenen Wert erhöhen. Dies ist ideal für die Erhöhung:

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

Google Kollektionen HashMultiset:
 - ganz elegant zu bedienen
 - aber verbrauchen CPU und Speicher

Am besten wäre es, ein Verfahren zu haben, wie: Entry<K,V> getOrPut(K); (Elegant und niedrig Kosten)

Ein solches Verfahren wird berechnet Hash und Index nur einmal, und dann könnten wir tun, was wir mit dem Eintrag wollen (Entweder ersetzen oder aktualisieren Sie den Wert).

Weitere elegant:
 - nehmen Sie ein HashSet<Entry>
 - erweitert es so, dass get(K) einen neuen Eintrag setzen, wenn nötig
 - Dieser könnte Ihr eigenes Objekt sein
. -> (new MyHashSet()).get(k).increment();

"put" müssen "get" (keine doppelten Schlüssel zu gewährleisten).
So tun direkt eine "put",
und wenn es ein vorheriger Wert ist, tut dann einen Zusatz:

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
}

Wenn Zählung bei 0 beginnt, fügen Sie dann 1: (oder irgendwelche andere Werte ...)

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
}

Hinweis: Dieser Code ist nicht Thread-sicher. Verwenden Sie es bauen dann die Karte verwenden, nicht gleichzeitig zu aktualisieren.

Optimierung:. In einer Schleife, hält alten Wert der neue Wert der nächsten Schleife zu werden

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

Die verschiedenen primitiven Wrapper, zB Integer unveränderlich ist, so gibt es wirklich keine prägnante Art und Weise zu tun, was Sie fragen es sei denn, Sie können es tun, mit so etwas wie Atomic . Ich kann in einer Minute und zu aktualisieren, dass ein Go geben. BTW, Hashtable ist ein Teil der Collections Framework .

würde ich Apache Sammlungen Faule Map verwenden (Werte auf 0 zu initialisieren) und verwenden MutableIntegers von Apache Lang als Werte in dieser Karte.

Biggest Kosten mit der Karte zweimal in Ihrer Methode zur Suche. In mir haben Sie es nur einmal zu tun. Nehmen Sie einfach den Wert (es wird, wenn abwesend erhalten initialisiert) und erhöhen es.

Die Functional Java Bibliothek TreeMap Datenstruktur hat eine update Methode in dem aktuellen Stamm Kopf:

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

Beispiel Nutzung:

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

Dieses Programm druckt "2".

@Vilmantas Baranauskas: In Bezug auf diese Antwort, würde ich kommentieren, wenn ich die rep Punkte hatte, aber ich weiß nicht. Ich wollte zu beachten, dass die Klasse Counter definiert es ist nicht Thread-sicher, da es nicht ausreichend ist, um nur inc () zu synchronisieren, ohne Wert zu synchronisieren (). Andere Themen Aufruf Wert () nicht den Wert zu sehen, es sei denn, garantiert ein geschieht zuvor Beziehung hat mit dem Update etabliert.

Ich weiß nicht, wie effizient es ist, aber der folgende Code funktioniert wie well.You eine BiFunction am Anfang definieren müssen. Darüber hinaus können Sie mehr machen als nur mit dieser Methode erhöht.

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

Ausgang

3
1

Wenn Sie mit Eclipse-Sammlungen , können Sie eine HashBag verwenden. Es wird der effizienteste Ansatz in Bezug auf die Speichernutzung, und es wird auch gut in Bezug auf die Ausführungsgeschwindigkeit durchführen.

HashBag wird von einem MutableObjectIntMap gesichert, die anstelle von Counter Objekte primitive Ints speichert. Dies reduziert Speicheraufwand und verbessert die Ausführungsgeschwindigkeit.

HashBag bietet die API Sie benötigen würde, da es ein Collection ist, die auch Sie für die Anzahl der Vorkommen eines Elements abfragen kann.

Hier ist ein Beispiel aus dem Eclipse-Sammlungen Kata .

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"));

. Hinweis: Ich bin ein Committer für Eclipse Sammlungen

Ganz einfach, benutzen Sie einfach die eingebaute Funktion in Map.java wie folgt

map.put(key, map.getOrDefault(key, 0) + 1);

Da eine Menge Leute für Groovy Antworten Java Themen suchen, hier ist, wie Sie es in Groovy tun können:

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}

Hope verstehe ich Ihre Frage richtig, ich bin zu Java von Python kommen, damit ich mit dem Kampf einfühlen kann.

Wenn Sie

map.put(key, 1)

Sie tun würden,

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

Hope, das hilft!

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top