Ich hoffe, diese Frage wird für dieses Forum nicht als zu grundlegend angesehen, aber wir werden sehen. Ich frage mich, wie ich Code für eine bessere Leistung umgestalten kann, die einige Male ausgeführt wird.
Angenommen, ich erstelle eine Worthäufigkeitsliste mit einer Map (wahrscheinlich einer HashMap), wobei jeder Schlüssel eine Zeichenfolge mit dem zu zählenden Wort ist 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}++;
In Java ist es jedoch viel komplizierter. Hier ist die Art, wie ich es gerade 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, um einen solchen Wert zu erhöhen. Gibt es überhaupt gute Leistungsgründe, um das Collections-Framework zu meiden und stattdessen etwas anderes zu verwenden?
Update: Ich habe einige der Antworten getestet. Siehe unten.
quelle
Antworten:
Einige Testergebnisse
Ich habe viele gute Antworten auf diese Frage erhalten - danke Leute -, also habe ich beschlossen, einige Tests durchzuführen und herauszufinden, welche Methode tatsächlich am schnellsten ist. Die fünf Methoden, die ich getestet habe, sind folgende:
Methode
Folgendes habe ich getan ...
Ergebnisse
Ich werde zuerst die Ergebnisse und den folgenden Code für diejenigen präsentieren, die interessiert sind.
Die ContainsKey- Methode war erwartungsgemäß die langsamste, daher gebe ich die Geschwindigkeit jeder Methode im Vergleich zur Geschwindigkeit dieser Methode an.
Schlussfolgerungen
Es scheint, dass nur die MutableInt-Methode und die Trove-Methode wesentlich schneller sind, da nur sie eine Leistungssteigerung von mehr als 10% bewirken. Wenn Threading jedoch ein Problem darstellt, ist AtomicLong möglicherweise attraktiver als die anderen (ich bin mir nicht sicher). Ich habe auch TestForNull mit
final
Variablen ausgeführt, aber der Unterschied war vernachlässigbar.Beachten Sie, dass ich die Speichernutzung in den verschiedenen Szenarien nicht profiliert habe. Ich würde mich freuen, von jedem zu hören, der gute Einblicke in die Auswirkungen der Methoden MutableInt und Trove auf die Speichernutzung hat.
Persönlich finde ich die MutableInt-Methode am attraktivsten, da keine Klassen von Drittanbietern geladen werden müssen. Wenn ich also keine Probleme damit entdecke, gehe ich wahrscheinlich so vor.
Der Code
Hier ist der entscheidende Code für jede Methode.
Enthält Schlüssel
TestForNull
AtomicLong
Fundgrube
MutableInt
quelle
freq.compute(word, (key, count) -> count == null ? 1 : count + 1)
? Intern führt es eine weniger gehashte Suche durch alscontainsKey
, es wäre interessant zu sehen, wie es sich aufgrund des Lambda im Vergleich zu anderen verhält.Jetzt gibt es einen kürzeren Weg mit Java 8
Map::merge
.Was es macht:
Weitere Informationen hier .
quelle
map.merge(key, 1, (a, b) -> a + b);
tat es aberInteger::sum
als BiFunction akzeptiert und @russter mochte es nicht, so zu antworten, wie es geschrieben wurde. Das hat bei mirMap.merge(key, 1, { a, b -> a + b})
Eine kleine Recherche im Jahr 2016: https://github.com/leventov/java-word-count , Benchmark-Quellcode
Beste Ergebnisse pro Methode (kleiner ist besser):
Zeit \ Raum Ergebnisse:
quelle
Google Guava ist dein Freund ...
... zumindest in einigen Fällen. Sie haben diese schöne AtomicLongMap . Besonders schön , weil man es zu tun hat lange als Wert in Ihrer Karte.
Z.B
Es ist auch möglich, dem Wert mehr als 1 hinzuzufügen:
quelle
AtomicLongMap#getAndAdd
nimmt ein Grundelementlong
und nicht die Wrapper-Klasse; Es macht keinen Sinn, das zu tunnew Long()
. UndAtomicLongMap
ist ein parametrisierter Typ; du hättest es als deklarieren sollenAtomicLongMap<String>
.@Hank Gay
Als Folge meines eigenen (ziemlich nutzlosen) Kommentars: Trove scheint der richtige Weg zu sein. Wenn aus irgendeinem Grund Sie mit dem Standard - JDK halten wollte, ConcurrentMap und Atomic kann der Code ein machen winziges bisschen schöner, obwohl YMMV.
wird
1
als Wert in der Karte für verlassenfoo
. Realistisch gesehen ist eine erhöhte Freundlichkeit beim Einfädeln alles, was dieser Ansatz empfehlen muss.quelle
Und so erhöhen Sie einen Wert mit einfachem Code.
Vorteil:
Nachteil:
Theoretisch wissen Sie nach dem Aufruf von get () bereits, wo Sie put () platzieren müssen, sodass Sie nicht erneut suchen müssen. Das Suchen in einer Hash-Map dauert jedoch normalerweise nur sehr wenig Zeit, damit Sie dieses Leistungsproblem ignorieren können.
Wenn Sie das Problem jedoch sehr ernst nehmen, sind Sie ein Perfektionist. Eine andere Möglichkeit ist die Verwendung der Zusammenführungsmethode. Dies ist (wahrscheinlich) effizienter als das vorherige Codefragment, da Sie die Karte (theoretisch) nur einmal durchsuchen werden: (obwohl) Dieser Code ist auf den ersten Blick nicht ersichtlich, er ist kurz und performant.
Vorschlag: Sie sollten sich in den meisten Fällen mehr um die Lesbarkeit des Codes als um einen geringen Leistungsgewinn kümmern. Wenn das erste Code-Snippet für Sie leichter zu verstehen ist, verwenden Sie es. Aber wenn Sie in der Lage sind, die zweite gut zu verstehen, können Sie es auch versuchen!
quelle
Es ist immer eine gute Idee, in der Google-Sammlungsbibliothek nach solchen Dingen zu suchen . In diesem Fall reicht ein Multiset aus :
Es gibt Map-ähnliche Methoden zum Durchlaufen von Schlüsseln / Einträgen usw. Intern verwendet die Implementierung derzeit a
HashMap<E, AtomicInteger>
, sodass keine Boxkosten anfallen.quelle
count()
Methode auf einem Multiset in O (1) oder O (n) Zeit (Worstcase)? Die Dokumente sind in diesem Punkt unklar.Sie sollten sich der Tatsache bewusst sein, dass Ihr ursprünglicher Versuch
enthält zwei potenziell teure Operationen auf einer Karte, nämlich
containsKey
undget
. Ersteres führt eine Operation aus, die möglicherweise der letzteren ziemlich ähnlich ist, sodass Sie dieselbe Arbeit zweimal ausführen !Wenn Sie sich die API für Map ansehen, werden
get
Operationen normalerweise zurückgegeben,null
wenn die Map das angeforderte Element nicht enthält.Beachten Sie, dass dies eine Lösung wie ergibt
gefährlich, da es
NullPointerException
s ergeben könnte . Sie sollten zuerst nachsehennull
.Beachten Sie auch , und dies ist sehr wichtig, dass
HashMap
s per Definition enthalten kannnulls
. Alsonull
sagt nicht jeder zurückgegebene "es gibt kein solches Element". In dieser HinsichtcontainsKey
verhält sich anders ausget
in dir eigentlich sagen , ob es ein solches Element ist. Weitere Informationen finden Sie in der API.In Ihrem Fall möchten Sie jedoch möglicherweise nicht zwischen einem gespeicherten
null
und einem "noSuchElement" unterscheiden. Wenn Sienull
s nicht zulassen möchten, bevorzugen Sie möglicherweise aHashtable
. Die Verwendung einer Wrapper-Bibliothek, wie sie bereits in anderen Antworten vorgeschlagen wurde, ist je nach Komplexität Ihrer Anwendung möglicherweise eine bessere Lösung für die manuelle Behandlung.Um die Antwort zu vervollständigen (und ich habe vergessen, dies zuerst dank der Bearbeitungsfunktion einzugeben!), Besteht der beste Weg, dies nativ zu tun, darin,
get
einefinal
Variable einzugeben, nach einer zu suchennull
undput
sie wieder mit a einzugeben1
. Die Variable sollte sein,final
weil sie sowieso unveränderlich ist. Der Compiler benötigt diesen Hinweis möglicherweise nicht, ist aber auf diese Weise klarer.Wenn Sie sich nicht auf Autoboxen verlassen möchten, sollten Sie
map.put(new Integer(1 + i.getValue()));
stattdessen etwas Ähnliches sagen .quelle
Ein anderer Weg wäre das Erstellen einer veränderlichen Ganzzahl:
Dies bedeutet natürlich, dass ein zusätzliches Objekt erstellt wird, aber der Overhead im Vergleich zum Erstellen einer Ganzzahl (auch mit Integer.valueOf) sollte nicht so hoch sein.
quelle
Sie können die computeIfAbsent- Methode in
Map
der in Java 8 bereitgestellten Schnittstelle verwenden .Die Methode
computeIfAbsent
prüft, ob der angegebene Schlüssel bereits einem Wert zugeordnet ist oder nicht. Wenn kein zugeordneter Wert vorhanden ist, wird versucht, seinen Wert mithilfe der angegebenen Zuordnungsfunktion zu berechnen. In jedem Fall wird der aktuelle (vorhandene oder berechnete) Wert zurückgegeben, der dem angegebenen Schlüssel zugeordnet ist, oder null, wenn der berechnete Wert null ist.Nebenbei bemerkt , wenn Sie eine Situation haben, in der mehrere Threads eine gemeinsame Summe aktualisieren, können Sie einen Blick auf die LongAdder- Klasse werfen. Bei hohen Konflikten ist der erwartete Durchsatz dieser Klasse erheblich höher als
AtomicLong
auf Kosten eines höheren Speicherplatzverbrauchs.quelle
Die Speicherrotation kann hier ein Problem sein, da jedes Boxen eines int größer oder gleich 128 eine Objektzuweisung verursacht (siehe Integer.valueOf (int)). Obwohl der Garbage Collector sehr effizient mit kurzlebigen Objekten umgeht, leidet die Leistung bis zu einem gewissen Grad.
Wenn Sie wissen, dass die Anzahl der vorgenommenen Inkremente die Anzahl der Schlüssel (in diesem Fall = Wörter) weit übersteigt, sollten Sie stattdessen einen int-Halter verwenden. Phax hat bereits Code dafür vorgelegt. Hier ist es wieder mit zwei Änderungen (Inhaberklasse statisch gemacht und Anfangswert auf 1 gesetzt):
Wenn Sie extreme Leistung benötigen, suchen Sie nach einer Map-Implementierung, die direkt auf primitive Werttypen zugeschnitten ist. jrudolph erwähnte GNU Trove .
Ein guter Suchbegriff für dieses Thema ist übrigens "Histogramm".
quelle
Anstatt includesKey () aufzurufen, ist es schneller, map.get aufzurufen und zu überprüfen, ob der zurückgegebene Wert null ist oder nicht.
quelle
Sind Sie sicher, dass dies ein Engpass ist? Haben Sie eine Leistungsanalyse durchgeführt?
Verwenden Sie den NetBeans-Profiler (kostenlos und in NB 6.1 integriert), um Hotspots anzuzeigen.
Schließlich ist ein JVM-Upgrade (z. B. von 1,5 bis 1,6) häufig ein billiger Leistungssteigerer. Selbst ein Upgrade der Build-Nummer kann zu guten Leistungssteigerungen führen. Wenn Sie unter Windows ausgeführt werden und dies eine Serverklassenanwendung ist, verwenden Sie -server in der Befehlszeile, um die Server-Hotspot-JVM zu verwenden. Auf Linux- und Solaris-Computern wird dies automatisch erkannt.
quelle
Es gibt verschiedene Ansätze:
Verwenden Sie einen Taschenalorithmus wie die in Google Collections enthaltenen Sätze.
Erstellen Sie einen veränderlichen Container, den Sie in der Map verwenden können:
Und benutze put ("Wort", neues Mein ("Wort")); Anschließend können Sie überprüfen, ob es vorhanden ist, und beim Hinzufügen erhöhen.
Vermeiden Sie es, Ihre eigene Lösung mithilfe von Listen zu erstellen, da Ihre Leistung stinkt, wenn Sie in Innerloops suchen und sortieren. Die erste HashMap-Lösung ist eigentlich ziemlich schnell, aber eine solche wie die in Google Collections ist wahrscheinlich besser.
Das Zählen von Wörtern mit Google Collections sieht ungefähr so aus:
Die Verwendung des HashMultiset ist sehr elegant, da ein Taschenalgorithmus genau das ist, was Sie zum Zählen von Wörtern benötigen.
quelle
Ich denke, Ihre Lösung wäre der Standardweg, aber - wie Sie selbst bemerkt haben - wahrscheinlich nicht der schnellste Weg.
Sie können sich GNU Trove ansehen . Das ist eine Bibliothek, die alle Arten von schnellen primitiven Sammlungen enthält. Ihr Beispiel würde eine TObjectIntHashMap verwenden, die eine Methode adjustOrPutValue hat, die genau das tut, was Sie wollen.
quelle
Eine Variation des MutableInt-Ansatzes, die bei einem Hack sogar noch schneller sein kann, ist die Verwendung eines Int-Arrays mit einem Element:
Es wäre interessant, wenn Sie Ihre Leistungstests mit dieser Variante wiederholen könnten. Es könnte das schnellste sein.
Bearbeiten: Das obige Muster hat für mich gut funktioniert, aber schließlich habe ich Troves Sammlungen verwendet, um die Speichergröße in einigen sehr großen Karten, die ich erstellt habe, zu reduzieren - und als Bonus war es auch schneller.
Eine wirklich nette Funktion ist, dass die
TObjectIntHashMap
Klasse einen einzelnenadjustOrPutValue
Aufruf hat, der, abhängig davon, ob dieser Schlüssel bereits einen Wert enthält, entweder einen Anfangswert setzt oder den vorhandenen Wert erhöht. Dies ist perfekt zum Inkrementieren:quelle
Google Collections HashMultiset:
- Sehr elegant zu bedienen
- verbrauchen aber CPU und Speicher
Am besten wäre eine Methode wie:
Entry<K,V> getOrPut(K);
(elegant und kostengünstig)Eine solche Methode berechnet Hash und Index nur einmal, und dann können wir mit dem Eintrag tun, was wir wollen (entweder den Wert ersetzen oder aktualisieren).
Eleganter:
- Nehmen Sie eine
HashSet<Entry>
- erweitern Sie sie so, dass Sie
get(K)
bei Bedarf einen neuen Eintrag einfügen.- Der Eintrag kann Ihr eigenes Objekt sein.
->
(new MyHashSet()).get(k).increment();
quelle
Ganz einfach, verwenden Sie einfach die eingebaute Funktion
Map.java
wie folgtquelle
++
erhöhen um ... OMG, es ist so einfach. @siegi++
nirgendwo in diesem Ausdruck, da eine Variable als Operand benötigt wird, aber nur Werte vorhanden sind. Ihre Hinzufügung von+ 1
Arbeiten. Jetzt ist Ihre Lösung dieselbe wie in der Antwort von off99555 ."put" muss "get" (um sicherzustellen, dass kein doppelter Schlüssel vorhanden ist).
Machen Sie also direkt einen "Put"
und wenn es einen vorherigen Wert gab, dann machen Sie einen Zusatz:
Wenn die Zählung bei 0 beginnt, addieren Sie 1: (oder andere Werte ...)
Hinweis: Dieser Code ist nicht threadsicher. Verwenden Sie es zum Erstellen und verwenden Sie dann die Karte, um sie nicht gleichzeitig zu aktualisieren.
Optimierung: Behalten Sie in einer Schleife den alten Wert bei, um der neue Wert der nächsten Schleife zu werden.
quelle
Die verschiedenen primitiven Wrapper sind z. B.
Integer
unveränderlich, so dass es wirklich keine präzisere Möglichkeit gibt, das zu tun, was Sie verlangen, es sei denn, Sie können es mit etwas wie AtomicLong tun . Ich kann das in einer Minute ausprobieren und aktualisieren. BTW, Hashtable ist ein Teil des Collections Framework .quelle
Ich würde Apache Collections Lazy Map verwenden (um Werte auf 0 zu initialisieren) und MutableIntegers von Apache Lang als Werte in dieser Map verwenden.
Die größten Kosten entstehen, wenn Sie die Karte in Ihrer Methode zweimal durchsuchen müssen. In meinem musst du es nur einmal machen. Holen Sie sich einfach den Wert (er wird initialisiert, wenn er nicht vorhanden ist) und erhöhen Sie ihn.
quelle
Die Datenstruktur der Functional Java- Bibliothek
TreeMap
verfügt über eineupdate
Methode im neuesten Trunk Head:Anwendungsbeispiel:
Dieses Programm gibt "2" aus.
quelle
@Vilmantas Baranauskas: In Bezug auf diese Antwort würde ich kommentieren, wenn ich die Wiederholungspunkte hätte, aber ich nicht. Ich wollte beachten, dass die dort definierte Counter-Klasse NICHT threadsicher ist, da es nicht ausreicht, nur inc () zu synchronisieren, ohne value () zu synchronisieren. Bei anderen Threads, die value () aufrufen, wird der Wert nur dann garantiert angezeigt, wenn eine Beziehung hergestellt wurde, bevor mit der Aktualisierung eine Beziehung hergestellt wurde.
quelle
Ich weiß nicht, wie effizient es ist, aber der folgende Code funktioniert auch. Sie müssen
BiFunction
am Anfang einen definieren . Außerdem können Sie mit dieser Methode mehr als nur inkrementieren.Ausgabe ist
quelle
Wenn Sie Eclipse-Sammlungen verwenden , können Sie a verwenden
HashBag
. Dies ist der effizienteste Ansatz in Bezug auf die Speichernutzung und eine gute Leistung in Bezug auf die Ausführungsgeschwindigkeit.HashBag
wird von a unterstützt,MutableObjectIntMap
das primitive Ints anstelle vonCounter
Objekten speichert . Dies reduziert den Speicheraufwand und verbessert die Ausführungsgeschwindigkeit.HashBag
bietet die API, die Sie benötigen, da es sich um eine handeltCollection
damit auch die Anzahl der Vorkommen eines Elements abfragen können.Hier ist ein Beispiel aus der Eclipse Collections Kata .
Hinweis: Ich bin ein Committer für Eclipse-Sammlungen.
quelle
Ich schlage vor, Java 8 Map :: compute () zu verwenden. Es wird auch der Fall berücksichtigt, in dem kein Schlüssel vorhanden ist.
quelle
mymap.merge(key, 1, Integer::sum)
?Da viele Leute in Java-Themen nach Groovy-Antworten suchen, können Sie dies in Groovy folgendermaßen tun:
quelle
Der einfache und einfache Weg in Java 8 ist der folgende:
quelle
Ich hoffe, ich verstehe Ihre Frage richtig. Ich komme von Python zu Java, damit ich mich in Ihren Kampf einfühlen kann.
Wenn Sie haben
Du würdest
Hoffe das hilft!
quelle