次のコードと、それを反復する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. #2
はmap
全体を反復処理するより効率的な手段です(ただし、間違っている可能性があります)
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ミリ秒かかりました
...等々
2番目のスニペットは、キーを再検索する必要がないため、わずかに高速になります。
すべてのHashMap
イテレータは nextEntry
メソッド を呼び出します。これは_Entry<K,V>
_を返します。
最初のスニペットは、エントリ( KeyIterator
内)から値を破棄し、辞書で再度検索します。
2番目のスニペットは、キーと値を直接使用します( EntryIterator
から)
( keySet()
と entrySet()
は両方とも安価な呼び出しです)
後者は前者よりも効率的です。 FindBugs のようなツールは、実際には前者にフラグを立て、後者の実行を推奨します。
地図:
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() ) {
...
}
一般的に、HashMapの場合、2番目の方が少し高速になります。 get(key)
呼び出しはO(1)
よりも遅くなるため、ハッシュの衝突が多い場合にのみ重要です-k
が同じバケット内のエントリの数(つまり、同じキーの数)でO(k)
を取得する同じバケットにマップされるハッシュコードまたは異なるハッシュコード-これは、マップの容量、サイズ、負荷係数にも依存します)。
エントリ反復型バリアントはルックアップを行う必要がないため、ここで少し速くなります。
別の注意:マップの容量が実際のサイズよりもはるかに大きく、反復を多く使用する場合は、代わりにLinkedHashMapを使用することを検討してください。完全な反復(予測可能な反復順序)の代わりにO(size)
の複雑さをO(size+capacity)
に提供します。 (要因が異なる可能性があるため、これで本当に改善されるかどうかを測定する必要があります。LinkedHashMapには、マップを作成するための大きなオーバーヘッドがあります。)
bguiz、
EntrySet(代替2)の反復は、値を取得するために各キーをハッシュしないため、わずかに効率的だと思います(わかりません)。 O(1)エントリごとの操作、したがって、話しているのはO(n) HashMap
...これはすべてHashMap
のみに適用されます... Map
の他の実装には、非常に異なるパフォーマンス特性があります。
パフォーマンスの違いに実際に気づくために、あなたは「それを押して」いると思います。心配な場合は、テストケースをセットアップして両方の反復手法の時間を計ってみませんか?
実際に報告されたパフォーマンスの問題がない場合は、あまり心配していません...ここでクロックを数回刻むと、プログラムの全体的な使いやすさに影響しません。
コードの多くの他の多くの側面は、通常、完全なパフォーマンスよりも重要だと思います。もちろん、いくつかのブロックは「パフォーマンスクリティカル」であり、これは書かれている前に知られており、単独でパフォーマンステストが行われます...しかし、そのようなケースはかなりまれです。一般的なアプローチとして、完全で、正確で、柔軟性があり、テスト可能で、再利用可能で、読み取り可能で、保守可能なコードを書くことに焦点を合わせることをお勧めします。
バージョン0は、「最適化」なしで、可能な限り単純でなければなりません。
(私のベンチマークによると)最も効率的な方法は、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.forEach
とHashMap.entrySet().forEach()
は大きなマップで最高のパフォーマンスを発揮し、entrySet()
のforループによって結合され、小さなマップで最高のパフォーマンスを発揮します。
キーメソッドが遅い理由は、おそらく各エントリの値を再度検索する必要があるためです。他のメソッドは、値を取得する必要があるオブジェクトのフィールドを読み取るだけでよいためです。イテレータメソッドの処理速度が遅くなると予想される理由は、外部反復を行っているため、各要素に対して2つのメソッド呼び出し(hasNext
およびnext
)が必要であり、反復状態を保存するためです。反復子オブジェクトでは、forEach
によって行われる内部反復には、accept
へのメソッド呼び出しが1つだけ必要です。
より正確な結果を得るには、ターゲットデータでターゲットハードウェアのプロファイルを作成し、ループでターゲットアクションを実行する必要があります。