この質問がこのフォーラムにとってあまりにも基本的なものではないと願っていますが、わかります。何度も実行されているパフォーマンスを向上させるために、コードをリファクタリングする方法を考えています。
Map(おそらくHashMap)を使用して、Wordの頻度リストを作成しているとします。各キーはカウント対象のWordを含む文字列で、値はWordのトークンが見つかるたびに増加する整数です。
Perlでは、このような値を増やすのは簡単です。
$map{$Word}++;
しかし、Javaでは、はるかに複雑です。これが私の現在のやり方です。
int count = map.containsKey(Word) ? map.get(Word) : 0;
map.put(Word, count + 1);
もちろんどちらが新しいJavaバージョンのオートボクシング機能に依存しています。あなたはそのような値を増加させるより効率的な方法を提案できるかどうか疑問に思います。 Collectionsフレームワークを回避し、代わりに他のものを使用することには、パフォーマンス上の理由があるでしょうか。
更新:私はいくつかの答えのテストをしました。下記参照。
私はこの質問に対する多くの良い答えを得ました - ありがとうございます - そこで私はいくつかのテストを実行し、どの方法が実際に最速かを判断することにしました。私がテストした5つの方法は次のとおりです。
これが私がしたことです...
興味がある人のために、最初に結果と以下のコードを提示します。
ContainsKeyメソッドは予想どおり最も遅いので、各メソッドのスピードをそのメソッドのスピードと比較して説明します。
MutableIntメソッドとTroveメソッドだけが、10%を超えるパフォーマンスの向上をもたらすという点で、はるかに高速です。ただし、スレッド化が問題になる場合は、AtomicLongが他のものよりも魅力的かもしれません(私は本当によくわかりません)。 TestForNullをfinal
変数でも実行しましたが、その違いはごくわずかでした。
さまざまなシナリオでメモリ使用量をプロファイルしていないことに注意してください。 MutableIntメソッドとTroveメソッドがメモリ使用量にどのような影響を与える可能性があるかについて、優れた洞察を持っている人からの連絡をお待ちしています。
個人的には、MutableIntメソッドが最も魅力的だと思います。サードパーティのクラスをロードする必要がないからです。それで私がそれに関する問題を発見しない限り、それは私が行く可能性が最も高い方法です。
これが各メソッドの重要なコードです。
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);
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);
}
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();
import gnu.trove.TObjectIntHashMap;
...
TObjectIntHashMap<String> freq = new TObjectIntHashMap<String>();
...
freq.adjustOrPutValue(Word, 1, 1);
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();
}
はい、古い質問かもしれませんが、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
時空間の結果:
...少なくとも場合によっては。彼らはこのNice AtomicLongMap を持っています。あなたがあなたのマップの値としてlongを扱っているので特にいいです。
例えば。
AtomicLongMap<String> map = AtomicLongMap.create();
[...]
map.getAndIncrement(Word);
値に1以上を追加することも可能です。
map.getAndAdd(Word, 112L);
@ハンクゲイ
私自身の(やや役に立たない)コメントへのフォローアップとして:Troveは行く道のように見えます。何らかの理由で標準のJDKを使いたければ、 ConcurrentMap と AtomicLong でコードをa にすることができます。小さい / YMMVよりも少し良い。
final ConcurrentMap<String, AtomicLong> map = new ConcurrentHashMap<String, AtomicLong>();
map.putIfAbsent("foo", new AtomicLong(0));
map.get("foo").incrementAndGet();
foo
のマップ内の値として1
を残します。現実的には、このアプローチで推奨されるのは、スレッド処理に対する親しみやすさの向上だけです。
このようなことについては、常に Google Collections Library をご覧になることをお勧めします。この場合、 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>
を使用しているので、ボクシングのコストは発生しません。
あなたは自分の最初の試みが
int count = map.containsKey(Word) map.get(ワード):0。
マップに対する2つの潜在的に高価な操作、つまりcontainsKey
とget
を含みます。前者は後者と潜在的にかなり似た操作を実行するので、あなたは同じ仕事をしている2回!
MapのAPIを見ると、マップに要求された要素が含まれていない場合、get
操作は通常null
を返します。
これはのような解決策になることに注意してください
map.put(key、map.get(key)+ 1);
NullPointerException
sになる可能性があるので危険です。最初にnull
を確認してください。
また注意しなさいHashMap
s canは定義上nulls
を含むことが非常に重要です。したがって、返されるすべてのnull
が「そのような要素がない」と言っているわけではありません。この点で、containsKey
は実際にあなたに伝えるという意味でget
から異なってを振る舞いますかどうかそのような要素があるかどうか。詳細はAPIを参照してください。
ただし、あなたの場合では、格納されたnull
と "noSuchElement"を区別したくないかもしれません。 null
sを許可したくない場合はHashtable
をお勧めします。アプリケーションの複雑さによっては、他の回答ですでに提案されているようにラッパーライブラリを使用することが手動処理のより良い解決策になるかもしれません。
答えを完成させるには(そして最初は編集機能のおかげで忘れてしまいました!)それをネイティブに行う最良の方法はget
をfinal
変数に入れ、null
を確認し、put
を1
で戻すことです。 。とにかく不変なので、変数はfinal
であるべきです。コンパイラはこのヒントを必要としないかもしれませんが、そのほうが明確です。
final HashMap map = generateRandomHashMap(); finalオブジェクトkey = fetchSomeKey(); final整数i = map.get(key); if(i != null){ map.put(i + 1); } else { //何かをする }
オートボクシングに頼らないのであれば、代わりにmap.put(new Integer(1 + i.getValue()));
のようなものを言うべきです。
Map<String, Integer> map = new HashMap<>();
String key = "a random key";
int count = map.getOrDefault(key, 0);
map.put(key, count + 1);
そしてそれが、単純なコードで値を増やす方法です。
メリット:
別の方法はmergeメソッドを使うことですが、これは単に値をインクリメントするには多すぎます。
map.merge(key, 1, (a,b) -> a+b);
提案:ほとんどの場合、パフォーマンスの向上よりもコードの可読性を気にする必要があります。
もう1つの方法は、可変整数を作成することです。
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を使用した場合でも)オーバーヘッドはそれほど大きくないはずです。
Java 8にあるMap
name__インターフェースで computeIfAbsent メソッドを使用することができます。
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
name__は、指定されたキーがすでに値に関連付けられているかどうかを確認します。関連付けられた値がなければ、与えられたマッピング関数を使ってその値を計算しようとします。いずれの場合も、指定されたキーに関連付けられている現在の(既存または計算された)値を返します。計算された値がnullの場合はnullを返します。
注意:複数のスレッドが共通の合計を更新する状況がある場合は、 LongAdder クラスを参照してください。競合が多い場合、このクラスの予想スループットは、 AtomicLong
name__、スペース使用量の増加を犠牲にして。
128以上のintをボックス化するたびにオブジェクトが割り当てられるため、ここではメモリの回転が問題になる可能性があります(Integer.valueOf(int)を参照)。ガベージコレクタは寿命の短いオブジェクトを非常に効率的に扱いますが、パフォーマンスはある程度低下します。
インクリメントの数がキーの数(この場合は単語)を大幅に上回っていることがわかっている場合は、代わりにintホルダーを使用することを検討してください。 Phaxはすでにこれのためのコードを提示しました。これもまた2つの変更を加えたものです(ホルダークラスを静的にし、初期値を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();
}
極端なパフォーマンスが必要な場合は、プリミティブ値型に直接調整されたMap実装を探してください。 jrudolphは GNU Trove と述べました。
ちなみに、このテーマの良い検索用語は「ヒストグラム」です。
ContainsKey()を呼び出す代わりに、map.getを呼び出して戻り値がnullかどうかを確認するだけのほうが速いです。
Integer count = map.get(Word);
if(count == null){
count = 0;
}
map.put(Word, count + 1);
いくつかの方法があります。
Googleコレクションに含まれているセットのようなバッグのアルゴリズムを使用してください。
Mapで使用できる可変コンテナを作成します。
class My{
String Word;
int count;
}
そしてput( "Word"、new My( "Word"));を使います。それからあなたはそれが存在するかどうかを確認し、追加するときに増分することができます。
リストを使用して独自の解決策を実行することは避けてください。インナループ検索およびソートを実行すると、パフォーマンスが悪くなるためです。最初のHashMapソリューションは実際には非常に高速ですが、Googleコレクションに見られるような適切なものがおそらくより優れています。
Googleコレクションを使用して単語を数えると、次のようになります。
HashMultiset s = new HashMultiset();
s.add("Word");
s.add("Word");
System.out.println(""+s.count("Word") );
HashMultisetの使用は非常に賢明です。なぜなら、バグアルゴリズムは単語を数えるときに必要なものだけだからです。
Google Collections HashMultiset:
- とてもエレガント
- しかしCPUとメモリを消費する
Entry<K,V> getOrPut(K);
(エレガント、そして低コスト)のようなメソッドを持つことが最善です。
そのようなメソッドはハッシュとインデックスを一度だけ計算するでしょう、そしてそれから私たちはエントリを使って欲しいことをすることができます(値を置き換えるか更新する)。
もっとエレガント:
- HashSet<Entry>
を取る
- 必要に応じてget(K)
が新しいエントリを追加するように拡張する
- エントリーはあなた自身のものかもしれません。
- > (new MyHashSet()).get(k).increment();
ちょっとしたハックであれば、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];
このバリエーションを使用してパフォーマンステストを再実行できるとしたら、面白いでしょう。最速かもしれません。
編集:上記のパターンは私のためにうまく働きました、しかし結局私が作成していたいくつかの非常に大きい地図でメモリサイズを減らすためにTroveのコレクションを使うように変更しました - そしておまけとしてそれはより速いです。
本当に素晴らしい機能の1つは、TObjectIntHashMap
クラスが単一のadjustOrPutValue
呼び出しを持つことです。これは、そのキーに値がすでにあるかどうかに応じて、初期値を設定するか、既存の値を増分します。これはインクリメントに最適です。
TObjectIntHashMap<String> map = new TObjectIntHashMap<String>();
...
map.adjustOrPutValue(key, 1, 1);
私はあなたの解決策が標準的な方法であると思います、しかし - あなたがあなた自身を指摘したように - それはおそらく最速の方法ではないでしょう。
GNU Trove を見てください。それはあらゆる種類の高速なプリミティブコレクションを含むライブラリです。あなたの例は TObjectIntHashMap を使うでしょう。これはメソッドadjustOrPutValueを持っています。
これがボトルネックであることを確認していますか?パフォーマンス分析をしましたか?
ホットスポットを調べるには、NetBeansプロファイラ(無料でNB 6.1に組み込まれている)を使用してください。
最後に、JVMのアップグレード(1.5から1.6へのアップグレードなど)は、多くの場合、安価なパフォーマンスの向上です。ビルド番号をアップグレードしても、パフォーマンスは大幅に向上します。 Windowsを実行しており、これがサーバークラスのアプリケーションである場合は、コマンドラインで-serverを使用してServer Hotspot JVMを使用します。 LinuxおよびSolarisマシンでは、これは自動検出されています。
非常に簡単ですが、以下のようにMap.Java
の組み込み関数を使うだけです。
map.put(key, map.getOrDefault(key, 0) + 1);
「重複」キーがないことを確認するために、「put」に「get」が必要です。
だから直接「プット」をしてください、
そして、以前の値があった場合は、追加をします。
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
}
Notice:このコードはスレッドセーフではありません。同時に更新するのではなく、マップを構築してから使用するために使用します。
最適化:ループ内で、次のループの新しい値になるように古い値を保持します。
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
}
}
Eclipse Collections を使っているなら、HashBag
を使うことができます。これは、メモリ使用量の観点からは最も効率的なアプローチであり、実行速度の点でもパフォーマンスが優れています。
HashBag
は、MutableObjectIntMap
オブジェクトの代わりにプリミティブ整数を格納するCounter
によって支えられています。これにより、メモリのオーバーヘッドが削減され、実行速度が向上します。
HashBag
はCollection
なので、アイテムの出現回数を問い合わせることもできるので、必要なAPIを提供します。
これは Eclipse Collections 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"));
注:私はEclipseコレクションのコミッターです。
私は(値を0に初期化するために)Apache Collections Lazy Mapを使い、そのマップの中の値としてApache LangからのMutableIntegersを使いたいと思います。
最大のコストは、あなたの方法で地図を2回検索しなければならないことです。私の中であなたは一度だけそれをしなければなりません。値を取得し(存在しない場合は初期化されます)、値を増やします。
Functional Java ライブラリのTreeMap
データ構造は、最新のトランクヘッドに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"を印刷します。
それが効率的かどうかはわかりませんが、以下のコードも同様に機能します。最初に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
さまざまなプリミティブラッパー、たとえばInteger
は不変であるため、求めていることを行うためのより簡潔な方法はありませんnlessAtomicLong 。すぐに確認して更新できます。ところで、 HashtableisCollections Framework の一部です。
@Vilmantas Baranauskas:この回答に関しては、担当者の意見があればコメントしますが、しません。ここで定義されているCounterクラスは、value()を同期せずにinc()を同期するだけでは不十分であるため、スレッドセーフではないことに注意したいと思いました。他のスレッドがvalue()を呼び出しても、更新との間にビフォアビフォア関係が確立されていない限り、その値を確実に参照することはできません。