[英]Java : Iteration through a HashMap, which is more efficient?
給定以下代碼,有兩種替代方法可以遍歷它,
這兩種方法之間有什么性能差異嗎?
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
alt. #2
是遍歷整個map
的更有效方法(但我可能是錯的)
您的第二個選項肯定更有效,因為與第一個選項中的 n 次相比,您只進行一次查找。
但是,沒有什么比盡可能嘗試更好的了。 所以這里 -
(不完美但足以驗證假設並在我的機器上)
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");
}
結果(一些有趣的)
使用int mapSize = 5000; int strLength = 5;
int mapSize = 5000; int strLength = 5;
Alt #1 耗時 26 毫秒
Alt #2 耗時 20 毫秒
與int mapSize = 50000; int strLength = 5;
int mapSize = 50000; int strLength = 5;
Alt #1 耗時 32 毫秒
Alt #2 耗時 20 毫秒
與int mapSize = 50000; int strLength = 50;
int mapSize = 50000; int strLength = 50;
Alt #1 耗時 22 毫秒
Alt #2 耗時 21 毫秒
與int mapSize = 50000; int strLength = 500;
int mapSize = 50000; int strLength = 500;
Alt #1 耗時 28 毫秒
Alt #2 耗時 23 毫秒
與int mapSize = 500000; int strLength = 5;
int mapSize = 500000; int strLength = 5;
Alt #1 耗時 92 毫秒
Alt #2 耗時 57 毫秒
...等等
第二個片段會稍微快一些,因為它不需要重新查找密鑰。
所有HashMap
迭代器都調用nextEntry
方法,該方法返回一個Entry<K,V>
。
您的第一個片段丟棄條目中的值(在KeyIterator
中),然后在字典中再次查找它。
您的第二個片段直接使用鍵和值(來自EntryIterator
)
( keySet()
和entrySet()
都是廉價調用)
Map:
Map<String, Integer> map = new HashMap<String, Integer>();
除了 2 個選項之外,還有一個選項。
1) keySet() - 如果您只需要使用鍵,請使用它
for ( String k : map.keySet() ) {
...
}
2) entrySet() - 如果你需要兩者都使用它:鍵和值
for ( Map.Entry<String, Integer> entry : map.entrySet() ) {
String k = entry.getKey();
Integer v = entry.getValue();
...
}
3) values() - 如果你只需要值就使用它
for ( Integer v : map.values() ) {
...
}
后者比前者效率更高。 FindBugs之類的工具實際上會標記前者並建議您執行后者。
布吉茲,
我認為(我不知道)迭代EntrySet(替代方案2)效率略高,僅僅是因為它不是hash每個鍵以獲得它的價值......話雖如此,計算hash是一個O (1) 每個條目的操作,因此我們只在整個HashMap
上談論 O(n) ...但請注意,所有這些僅適用於HashMap
... Map
的其他實現可能具有不同的性能特征。
我確實認為你會“推動它”來真正注意到性能上的差異。 如果您擔心,那么為什么不設置一個測試用例來計時這兩種迭代技術呢?
如果您沒有真正的、報告的性能問題,那么您真的擔心的不是很多……這里和那里的幾個時鍾滴答聲不會影響程序的整體可用性。
我相信代碼的許多其他方面通常比直接性能更重要。 當然,有些塊是“性能關鍵的”,這在編寫之前就已經知道了,更不用說性能測試了……但這種情況相當罕見。 作為一種通用方法,最好專注於編寫完整、正確、靈活、可測試、可重用、可讀、可維護的代碼……性能可以在以后根據需要構建。
版本 0 應該盡可能簡單,沒有任何“優化”。
一般來說,對於 HashMap,第二個會快一些。 僅當您有很多 hash 沖突時才真正重要,因為此后get(key)
調用變得比O(1)
慢 - 它得到O(k)
,其中k
是同一存儲桶中的條目數(即數字具有相同 hash 代碼或不同 hash 代碼的密鑰仍然映射到同一個存儲桶 - 這也取決於 map 的容量、大小和負載因子)
入口迭代變體不必進行查找,因此它在這里變得更快一些。
另一個注意事項:如果您的 map 的容量比實際大小大很多並且您經常使用迭代,您可以考慮使用 LinkedHashMap 代替。 它為完整的迭代(以及可預測的迭代順序)提供O(size)
而不是O(size+capacity)
復雜度。 (您仍然應該衡量這是否真的帶來了改進,因為這些因素可能會有所不同。LinkedHashMap 創建 map 的開銷更大。)
最有效的方法(根據我的基准)是使用在 Java 8 或HashMap.entrySet().forEach()
中添加的新HashMap.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 循環加入,在小地圖上表現最佳。
鍵方法較慢的原因可能是因為它們必須為每個條目再次查找值,而其他方法只需要讀取 object 中的字段,它們已經必須獲取值。 我希望迭代器方法更慢的原因是它們正在進行外部迭代,這需要對每個元素進行兩次方法調用( hasNext
和next
),並將迭代 state 存儲在迭代器 object 中,而內部迭代完成通過forEach
只需要一個方法調用就可以accept
。
您應該使用目標數據在目標硬件上進行概要分析,並在循環中執行目標操作以獲得更准確的結果。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.