ArrayList与LinkedList
我必須承認這篇文章的標題有點吸引人。 我最近閱讀了此博客文章 ,這是有關此主題的討論和辯論的一個很好的摘要。
但是這次,我想嘗試一種不同的方法來比較這兩個眾所周知的數據結構:使用硬件性能計數器 。
我不會進行微基準測試,也不能直接進行。 我不會使用System.nanoTime()計時,而是使用HPC(例如高速緩存命中/未命中)。
無需介紹這些數據結構,每個人都知道它們的用途以及實現方式。 我將研究重點放在列表迭代上,因為除了添加元素之外,這是列表最常見的任務。 同時也因為列表的內存訪問模式是CPU緩存交互的一個很好的例子。
這是我的用于測量LinkedList和ArrayList的列表迭代的代碼:
import java.util.ArrayList; import java.util.LinkedList; import java.util.List;import ch.usi.overseer.OverHpc;public class ListIteration {private static List<String> arrayList = new ArrayList<>();private static List<String> linkedList = new LinkedList<>();public static void initializeList(List<String> list, int bufferSize){for (int i = 0; i < 50000; i++){byte[] buffer = null;if (bufferSize > 0){buffer = new byte[bufferSize];}String s = String.valueOf(i);list.add(s);// avoid buffer to be optimized awayif (System.currentTimeMillis() == 0){System.out.println(buffer);}}}public static void bench(List<String> list){if (list.contains("bar")){System.out.println("bar found");}}public static void main(String[] args) throws Exception{if (args.length != 2) return;List<String> benchList = "array".equals(args[0]) ? arrayList : linkedList;int bufferSize = Integer.parseInt(args[1]);initializeList(benchList, bufferSize);HWCounters.init();System.out.println("init done");// warmupfor (int i = 0; i < 10000; i++){bench(benchList);}Thread.sleep(1000);System.out.println("warmup done");HWCounters.start();for (int i = 0; i < 1000; i++){bench(benchList);}HWCounters.stop();HWCounters.printResults();HWCounters.shutdown();} }為了進行測量,我使用基于監督程序庫的名為HWCounters的類來獲取硬件性能計數器。 您可以在這里找到此類的代碼。
該程序采用2個參數:第一個參數用于ArrayList實現或LinkedList之間的選擇,第二個參數用于initializeList方法中使用的緩沖區大小。 此方法使用50K字符串填充列表實現。 每個字符串都是剛創建的,即將添加到列表中。 我們也可以根據程序的第二個參數分配一個緩沖區。 如果為0,則不分配緩沖區。
bench方法執行對列表中未包含的常量字符串的搜索,因此我們完全遍歷了列表。
最后, main方法是執行列表的初始化,對基準方法進行預熱并測量該方法的1000次運行。 然后,我們從HPC打印結果。
讓我們在不帶2 Xeon X5680的Linux上不分配緩沖區的情況下運行程序:
[root@archi-srv]# java -cp .:overseer.jar com.ullink.perf.myths.ListIteration?array 0 init done warmup done Cycles: 428,711,720 Instructions: 776,215,597 L2 hits: 5,302,792 L2 misses: 23,702,079 LLC hits: 42,933,789 LLC misses: 73 CPU migrations: 0 Local DRAM: 0 Remote DRAM: 0[root@archi-srv]# /java -cp .:overseer.jar com.ullink.perf.myths.ListIteration?linked 0 init done warmup done Cycles: 767,019,336 Instructions: 874,081,196 L2 hits: 61,489,499 L2 misses: 2,499,227 LLC hits: 3,788,468 LLC misses: 0 CPU migrations: 0 Local DRAM: 0 Remote DRAM: 0第一次運行是在ArrayList實現上,第二次是使用LinkedList。
- 周期數是執行代碼所花費的CPU周期數。 顯然,LinkedList比ArrayList花費了更多的周期。
- LinkedList的說明要高一些。 但這在這里并不重要。
- 對于L2緩存訪問,我們有一個明顯的區別:與LinkedList相比,ArrayList的L2未命中率要高得多。
- 從機械上講,LLC命中對ArrayList非常重要。
進行此比較的結論是,列表迭代期間訪問的大多數數據位于LinkedList的L2中,但位于ArrayList的L3中。
我對此的解釋是,添加到列表中的字符串是在之前創建的。 對于LinkedList,這意味著它是本地的在添加元素時創建的Node條目。 我們在節點上有更多位置。
但是,讓我們使用為每個新添加的String分配的中間緩沖區重新運行比較。
[root@archi-srv]# java -cp .:overseer.jar com.ullink.perf.myths.ListIteration array 256 init done warmup done Cycles: 584,965,201 Instructions: 774,373,285 L2 hits: 952,193 L2 misses: 62,840,804 LLC hits: 63,126,049 LLC misses: 4,416 CPU migrations: 0 Local DRAM: 824 Remote DRAM: 0[root@archi-srv]# java -cp .:overseer.jar com.ullink.perf.myths.ListIteration linked 256 init done warmup done Cycles: 5,289,317,879 Instructions: 874,350,022 L2 hits: 1,487,037 L2 misses: 75,500,984 LLC hits: 81,881,688 LLC misses: 5,826,435 CPU migrations: 0 Local DRAM: 1,645,436 Remote DRAM: 1,042這里的結果有很大的不同:
- 循環的重要性提高了10倍。
- 說明與以前相同
- 對于緩存訪問,ArrayList具有比先前運行更多的L2未命中/ LLC命中,但仍處于相同的數量級順序。 相反,LinkedList具有更多的L2未命中/ LLC命中,但此外,還有相當數量的LLC未命中/ DRAM訪問。 區別就在這里。
使用中間緩沖區,我們可以推開條目和字符串,這會產生更多的高速緩存未命中,并且最終還會訪問DRAM,這比訪問高速緩存要慢得多。
ArrayList在這里更可預測,因為我們彼此之間保持元素的局部性。
此處的內存訪問模式對于列表迭代性能至關重要。 ArrayList比LinkedList更穩定,因為在每個元素添加之間進行任何操作,都可以使數據保持比LinkedList更本地。
還要記住,對數組進行迭代對于CPU而言效率要高得多,因為它可以觸發硬件預取,因為訪問模式是非常可預測的。
翻譯自: https://www.javacodegeeks.com/2013/12/arraylist-vs-linkedlist.html
總結
以上是生活随笔為你收集整理的ArrayList与LinkedList的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: cvvhdf参数设置(如何设置cv参数)
- 下一篇: 在测试中使用匹配器