web-dev-qa-db-ja.com

JavaでMap値を増やす最も効率的な方法

この質問がこのフォーラムにとってあまりにも基本的なものではないと願っていますが、わかります。何度も実行されているパフォーマンスを向上させるために、コードをリファクタリングする方法を考えています。

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フレームワークを回避し、代わりに他のものを使用することには、パフォーマンス上の理由があるでしょうか。

更新:私はいくつかの答えのテストをしました。下記参照。

327
gregory

いくつかのテスト結果

私はこの質問に対する多くの良い答えを得ました - ありがとうございます - そこで私はいくつかのテストを実行し、どの方法が実際に最速かを判断することにしました。私がテストした5つの方法は次のとおりです。

  • 私が提示した "ContainsKey"メソッド 質問
  • aleksandar Dimitrovによって提案された "TestForNull"メソッド
  • hank Gayが提案した "AtomicLong"メソッド
  • jrudolphが提案した "Trove"メソッド
  • phax.myopenid.comによって提案された "MutableInt"メソッド

方法

これが私がしたことです...

  1. 以下に示す違いを除いて同一の5つのクラスを作成しました。各クラスは、私が提示したシナリオの典型的な操作を実行しなければなりませんでした:10MBのファイルを開いてそれを読み込んで、それからファイルの中のすべてのWordトークンの頻度カウントを実行しました。これは平均3秒しかかからなかったので、頻度カウント(入出力ではない)を10回実行させました。
  2. 10回の繰り返しの時間を計りましたが、I/O操作はしませんでした。基本的に---使用した合計時間(クロック秒)を Java CookbookのIan Darwinの方法
  3. 5つすべてのテストを連続して実行してから、これをさらに3回実行しました。
  4. 各方法について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%を超えるパフォーマンスの向上をもたらすという点で、はるかに高速です。ただし、スレッド化が問題になる場合は、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();
}
344
gregory

はい、古い質問かもしれませんが、Java 8ではもっと短い方法があります。

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

機能:キーが存在しない場合は、値として1を入力します。そうでなければ、1キーにリンクされた値に合計します。より詳しい情報 ここ

175
LE GALL Benoît

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

時空間の結果: 

42
leventov

Google Guava はあなたの友達です...

...少なくとも場合によっては。彼らはこのNice AtomicLongMap を持っています。あなたがあなたのマップの値としてlongを扱っているので特にいいです。

例えば。

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

値に1以上を追加することも可能です。

map.getAndAdd(Word, 112L); 
33
H6.

@ハンクゲイ

私自身の(やや役に立たない)コメントへのフォローアップとして:Troveは行く道のように見えます。何らかの理由で標準のJDKを使いたければ、 ConcurrentMapAtomicLong でコードをa にすることができます。小さい / YMMVよりも少し良い。

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

fooのマップ内の値として1を残します。現実的には、このアプローチで推奨されるのは、スレッド処理に対する親しみやすさの向上だけです。

31
Hank Gay

このようなことについては、常に 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>を使用しているので、ボクシングのコストは発生しません。

25
Chris Nokleberg

あなたは自分の最初の試みが

int count = map.containsKey(Word) map.get(ワード):0。

マップに対する2つの潜在的に高価な操作、つまりcontainsKeygetを含みます。前者は後者と潜在的にかなり似た操作を実行するので、あなたは同じ仕事をしている2回

MapのAPIを見ると、マップに要求された要素が含まれていない場合、get操作は通常nullを返します。

これはのような解決策になることに注意してください

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

NullPointerExceptionsになる可能性があるので危険です。最初にnullを確認してください。

また注意しなさいHashMaps canは定義上nullsを含むことが非常に重要です。したがって、返されるすべてのnullが「そのような要素がない」と言っているわけではありません。この点で、containsKeyは実際にあなたに伝えるという意味でgetから異なってを振る舞いますかどうかそのような要素があるかどうか。詳細はAPIを参照してください。

ただし、あなたの場合では、格納されたnullと "noSuchElement"を区別したくないかもしれません。 nullsを許可したくない場合はHashtableをお勧めします。アプリケーションの複雑さによっては、他の回答ですでに提案されているようにラッパーライブラリを使用することが手動処理のより良い解決策になるかもしれません。

答えを完成させるには(そして最初は編集機能のおかげで忘れてしまいました!)それをネイティブに行う最良の方法はgetfinal変数に入れ、nullを確認し、put1で戻すことです。 。とにかく不変なので、変数は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()));のようなものを言うべきです。

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

そしてそれが、単純なコードで値を増やす方法です。

メリット:

  • 可変のintのために別のクラスを作成しない
  • ショートコード
  • わかりやすい
  • Nullポインタ例外なし

別の方法はmergeメソッドを使うことですが、これは単に値をインクリメントするには多すぎます。

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

提案:ほとんどの場合、パフォーマンスの向上よりもコードの可読性を気にする必要があります。

20
off99555

もう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を使用した場合でも)オーバーヘッドはそれほど大きくないはずです。

18
Philip Helger

Java 8にあるMapname__インターフェースで 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]

メソッドcomputeIfAbsentname__は、指定されたキーがすでに値に関連付けられているかどうかを確認します。関連付けられた値がなければ、与えられたマッピング関数を使ってその値を計算しようとします。いずれの場合も、指定されたキーに関連付けられている現在の(既存または計算された)値を返します。計算された値がnullの場合はnullを返します。

注意:複数のスレッドが共通の合計を更新する状況がある場合は、 LongAdder クラスを参照してください。競合が多い場合、このクラスの予想スループットは、 AtomicLongname__、スペース使用量の増加を犠牲にして。

9
i_am_zero

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 と述べました。

ちなみに、このテーマの良い検索用語は「ヒストグラム」です。

7
volley

ContainsKey()を呼び出す代わりに、map.getを呼び出して戻り値がnullかどうかを確認するだけのほうが速いです。

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

いくつかの方法があります。

  1. Googleコレクションに含まれているセットのようなバッグのアルゴリズムを使用してください。

  2. 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の使用は非常に賢明です。なぜなら、バグアルゴリズムは単語を数えるときに必要なものだけだからです。

3
tovare

Google Collections HashMultiset:
- とてもエレガント
- しかしCPUとメモリを消費する

Entry<K,V> getOrPut(K);(エレガント、そして低コスト)のようなメソッドを持つことが最善です。

そのようなメソッドはハッシュとインデックスを一度だけ計算するでしょう、そしてそれから私たちはエントリを使って欲しいことをすることができます(値を置き換えるか更新する)。

もっとエレガント:
- HashSet<Entry>を取る
- 必要に応じてget(K)が新しいエントリを追加するように拡張する
- エントリーはあなた自身のものかもしれません。
- > (new MyHashSet()).get(k).increment();

3
the felis leo

ちょっとしたハックであれば、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を持っています。

3
jrudolph

これがボトルネックであることを確認していますか?パフォーマンス分析をしましたか?

ホットスポットを調べるには、NetBeansプロファイラ(無料でNB 6.1に組み込まれている)を使用してください。

最後に、JVMのアップグレード(1.5から1.6へのアップグレードなど)は、多くの場合、安価なパフォーマンスの向上です。ビルド番号をアップグレードしても、パフォーマンスは大幅に向上します。 Windowsを実行しており、これがサーバークラスのアプリケーションである場合は、コマンドラインで-serverを使用してServer Hotspot JVMを使用します。 LinuxおよびSolarisマシンでは、これは自動検出されています。

3
John Wright

非常に簡単ですが、以下のようにMap.Javaの組み込み関数を使うだけです。

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

「重複」キーがないことを確認するために、「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
  }
}
2
the felis leo

Eclipse Collections を使っているなら、HashBagを使うことができます。これは、メモリ使用量の観点からは最も効率的なアプローチであり、実行速度の点でもパフォーマンスが優れています。

HashBagは、MutableObjectIntMapオブジェクトの代わりにプリミティブ整数を格納するCounterによって支えられています。これにより、メモリのオーバーヘッドが削減され、実行速度が向上します。

HashBagCollectionなので、アイテムの出現回数を問い合わせることもできるので、必要な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コレクションのコミッターです。

1
Craig P. Motlin

私は(値を0に初期化するために)Apache Collections Lazy Mapを使い、そのマップの中の値としてApache LangからのMutableIntegersを使いたいと思います。

最大のコストは、あなたの方法で地図を2回検索しなければならないことです。私の中であなたは一度だけそれをしなければなりません。値を取得し(存在しない場合は初期化されます)、値を増やします。

1
jb.

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"を印刷します。

1
Apocalisp

それが効率的かどうかはわかりませんが、以下のコードも同様に機能します。最初に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
1
MGoksu

さまざまなプリミティブラッパー、たとえばIntegerは不変であるため、求めていることを行うためのより簡潔な方法はありませんnlessAtomicLong 。すぐに確認して更新できます。ところで、 HashtableisCollections Framework の一部です。

1
Hank Gay

@Vilmantas Baranauskas:この回答に関しては、担当者の意見があればコメントしますが、しません。ここで定義されているCounterクラスは、value()を同期せずにinc()を同期するだけでは不十分であるため、スレッドセーフではないことに注意したいと思いました。他のスレッドがvalue()を呼び出しても、更新との間にビフォアビフォア関係が確立されていない限り、その値を確実に参照することはできません。

1
Alex Miller