web-dev-qa-db-ja.com

Java:HashMapを介した反復、どちらがより効率的ですか?

次のコードと、それを反復する2つの代替方法を考えると、
これら2つの方法の間にパフォーマンスの違いはありますか?

        Map<String, Integer> map = new HashMap<String, Integer>();
        //populate map

        //alt. #1
        for (String key : map.keySet())
        {
            Integer value = map.get(key);
            //use key and value
        }

        //alt. #2
        for (Map.Entry<String, Integer> entry : map.entrySet())
        {
            String key = entry.getKey();
            Integer value = entry.getValue();
            //use key and value
        }

私はalt. #2map全体を反復処理するより効率的な手段です(ただし、間違っている可能性があります)

56
bguiz

2番目のオプションは、最初のオプションでn回の検索と比較して1回だけ検索を行うため、間違いなくより効率的です。

しかし、できるときに試してみることほど良いものはありません。だからここに行く-

(完璧ではありませんが、とにかく仮定を検証するには十分です)

public static void main(String args[]) {

    Map<String, Integer> map = new HashMap<String, Integer>();
    // populate map

    int mapSize = 500000;
    int strLength = 5;
    for(int i=0;i<mapSize;i++)
        map.put(RandomStringUtils.random(strLength), RandomUtils.nextInt());

    long start = System.currentTimeMillis();
    // alt. #1
    for (String key : map.keySet()) {
        Integer value = map.get(key);
        // use key and value
    }
    System.out.println("Alt #1 took "+(System.currentTimeMillis()-start)+" ms");

    start = System.currentTimeMillis();
    // alt. #2
    for (Map.Entry<String, Integer> entry : map.entrySet()) {
        String key = entry.getKey();
        Integer value = entry.getValue();
        // use key and value
    }
    System.out.println("Alt #2 took "+(System.currentTimeMillis()-start)+" ms");
}

[〜#〜] results [〜#〜](興味深いもの)

int mapSize = 5000; int strLength = 5;
Alt#1には26ミリ秒かかりました
Alt#2は20ミリ秒かかりました

int mapSize = 50000; int strLength = 5;
Alt#1には32ミリ秒かかりました
Alt#2は20ミリ秒かかりました

int mapSize = 50000; int strLength = 50;
Alt#1は22ミリ秒かかりました
Alt#2は21ミリ秒かかりました

int mapSize = 50000; int strLength = 500;
Alt#1には28ミリ秒かかりました
Alt#2は23ミリ秒かかりました

int mapSize = 500000; int strLength = 5;
Alt#1には92ミリ秒かかりました
Alt#2には57ミリ秒かかりました

...等々

59
Amol Katdare

2番目のスニペットは、キーを再検索する必要がないため、わずかに高速になります。

すべてのHashMapイテレータは nextEntryメソッド を呼び出します。これは_Entry<K,V>_を返します。

最初のスニペットは、エントリ( KeyIterator 内)から値を破棄し、辞書で再度検索します。

2番目のスニペットは、キーと値を直接使用します( EntryIterator から)

keySet()entrySet() は両方とも安価な呼び出しです)

10
SLaks

後者は前者よりも効率的です。 FindBugs のようなツールは、実際には前者にフラグを立て、後者の実行を推奨します。

5
Jonas Kongslund

地図:

Map<String, Integer> map = new HashMap<String, Integer>();

2つのオプションのほかに、もう1つあります。

1)keySet()-使用する必要がある場合に使用onlykeys

for ( String k : map.keySet() ) {
    ...
}

2)entrySet()-両方が必要な場合に使用します:keys&values

for ( Map.Entry<String, Integer> entry : map.entrySet() ) {
    String k = entry.getKey();
    Integer v = entry.getValue();
    ...
}

3)values()-必要な場合に使用onlyvalues

for ( Integer v : map.values() ) {
    ...
}
5

一般的に、HashMapの場合、2番目の方が少し高速になります。 get(key)呼び出しはO(1)よりも遅くなるため、ハッシュの衝突が多い場合にのみ重要です-kが同じバケット内のエントリの数(つまり、同じキーの数)でO(k)を取得する同じバケットにマップされるハッシュコードまたは異なるハッシュコード-これは、マップの容量、サイズ、負荷係数にも依存します)。

エントリ反復型バリアントはルックアップを行う必要がないため、ここで少し速くなります。

別の注意:マップの容量が実際のサイズよりもはるかに大きく、反復を多く使用する場合は、代わりにLinkedHashMapを使用することを検討してください。完全な反復(予測可能な反復順序)の代わりにO(size)の複雑さをO(size+capacity)に提供します。 (要因が異なる可能性があるため、これで本当に改善されるかどうかを測定する必要があります。LinkedHashMapには、マップを作成するための大きなオーバーヘッドがあります。)

2
Paŭlo Ebermann

bguiz、

EntrySet(代替2)の反復は、値を取得するために各キーをハッシュしないため、わずかに効率的だと思います(わかりません)。 O(1)エントリごとの操作、したがって、話しているのはO(n) HashMap...これはすべてHashMapのみに適用されます... Mapの他の実装には、非常に異なるパフォーマンス特性があります。

パフォーマンスの違いに実際に気づくために、あなたは「それを押して」いると思います。心配な場合は、テストケースをセットアップして両方の反復手法の時間を計ってみませんか?

実際に報告されたパフォーマンスの問題がない場合は、あまり心配していません...ここでクロックを数回刻むと、プログラムの全体的な使いやすさに影響しません。

コードの多くの他の多くの側面は、通常、完全なパフォーマンスよりも重要だと思います。もちろん、いくつかのブロックは「パフォーマンスクリティカル」であり、これは書かれている前に知られており、単独でパフォーマンステストが行​​われます...しかし、そのようなケースはかなりまれです。一般的なアプローチとして、完全で、正確で、柔軟性があり、テスト可能で、再利用可能で、読み取り可能で、保守可能なコードを書くことに焦点を合わせることをお勧めします。

バージョン0は、「最適化」なしで、可能な限り単純でなければなりません。

2
corlettk

(私のベンチマークによると)最も効率的な方法は、Java 8またはHashMap.forEach()]に追加された新しい HashMap.entrySet().forEach() メソッドを使用することです。

JMHベンチマーク:

@Param({"50", "500", "5000", "50000", "500000"})
int limit;
HashMap<String, Integer> m = new HashMap<>();
public Test() {
}
@Setup(Level.Trial)
public void setup(){
    m = new HashMap<>(m);
    for(int i = 0; i < limit; i++){
        m.put(i + "", i);
    }
}
int i;
@Benchmark
public int forEach(Blackhole b){
    i = 0;
    m.forEach((k, v) -> { i += k.length() + v; });
    return i;
}
@Benchmark
public int keys(Blackhole b){
    i = 0;
    for(String key : m.keySet()){ i += key.length() + m.get(key); }
    return i;
}
@Benchmark
public int entries(Blackhole b){
    i = 0;
    for (Map.Entry<String, Integer> entry : m.entrySet()){ i += entry.getKey().length() + entry.getValue(); }
    return i;
}
@Benchmark
public int keysForEach(Blackhole b){
    i = 0;
    m.keySet().forEach(key -> { i += key.length() + m.get(key); });
    return i;
}
@Benchmark
public int entriesForEach(Blackhole b){
    i = 0;
    m.entrySet().forEach(entry -> { i += entry.getKey().length() + entry.getValue(); });
    return i;
}
public static void main(String[] args) throws RunnerException {
    Options opt = new OptionsBuilder()
            .include(Test.class.getSimpleName())
            .forks(1)
            .warmupIterations(25)
            .measurementIterations(25)
            .measurementTime(TimeValue.milliseconds(1000))
            .warmupTime(TimeValue.milliseconds(1000))
            .timeUnit(TimeUnit.MICROSECONDS)
            .mode(Mode.AverageTime)
            .build();
    new Runner(opt).run();
}

結果:

Benchmark            (limit)  Mode  Cnt      Score    Error  Units
Test.entries              50  avgt   25      0.282 ±  0.037  us/op
Test.entries             500  avgt   25      2.792 ±  0.080  us/op
Test.entries            5000  avgt   25     29.986 ±  0.256  us/op
Test.entries           50000  avgt   25   1070.218 ±  5.230  us/op
Test.entries          500000  avgt   25   8625.096 ± 24.621  us/op
Test.entriesForEach       50  avgt   25      0.261 ±  0.008  us/op
Test.entriesForEach      500  avgt   25      2.891 ±  0.007  us/op
Test.entriesForEach     5000  avgt   25     31.667 ±  1.404  us/op
Test.entriesForEach    50000  avgt   25    664.416 ±  6.149  us/op
Test.entriesForEach   500000  avgt   25   5337.642 ± 91.186  us/op
Test.forEach              50  avgt   25      0.286 ±  0.001  us/op
Test.forEach             500  avgt   25      2.847 ±  0.009  us/op
Test.forEach            5000  avgt   25     30.923 ±  0.140  us/op
Test.forEach           50000  avgt   25    670.322 ±  7.532  us/op
Test.forEach          500000  avgt   25   5450.093 ± 62.384  us/op
Test.keys                 50  avgt   25      0.453 ±  0.003  us/op
Test.keys                500  avgt   25      5.045 ±  0.060  us/op
Test.keys               5000  avgt   25     58.485 ±  3.687  us/op
Test.keys              50000  avgt   25   1504.207 ± 87.955  us/op
Test.keys             500000  avgt   25  10452.425 ± 28.641  us/op
Test.keysForEach          50  avgt   25      0.567 ±  0.025  us/op
Test.keysForEach         500  avgt   25      5.743 ±  0.054  us/op
Test.keysForEach        5000  avgt   25     61.234 ±  0.171  us/op
Test.keysForEach       50000  avgt   25   1142.416 ±  3.494  us/op
Test.keysForEach      500000  avgt   25   8622.734 ± 40.842  us/op

ご覧のとおり、HashMap.forEachHashMap.entrySet().forEach()は大きなマップで最高のパフォーマンスを発揮し、entrySet()のforループによって結合され、小さなマップで最高のパフォーマンスを発揮します。

キーメソッドが遅い理由は、おそらく各エントリの値を再度検索する必要があるためです。他のメソッドは、値を取得する必要があるオブジェクトのフィールドを読み取るだけでよいためです。イテレータメソッドの処理速度が遅くなると予想される理由は、外部反復を行っているため、各要素に対して2つのメソッド呼び出し(hasNextおよびnext)が必要であり、反復状態を保存するためです。反復子オブジェクトでは、forEachによって行われる内部反復には、acceptへのメソッド呼び出しが1つだけ必要です。

より正確な結果を得るには、ターゲットデータでターゲットハードウェアのプロファイルを作成し、ループでターゲットアクションを実行する必要があります。

1
Alex