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 away
            if (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");
        // warmup
        for (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而言效率要高得多,因为它可以触发硬件预取,因为访问模式是非常可预测的。

Java出现日历博客上的JCG合作伙伴 Jean-Philippe BEMPEL的参考: ArrayList与LinkedList

翻译自: https://www.javacodegeeks.com/2013/12/arraylist-vs-linkedlist.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值