Java性能优化-测试数组和链表在查询和添加删除时性能对比

场景

Java中使用JMH(Java Microbenchmark Harness 微基准测试框架)进行性能测试和优化:

Java中使用JMH(Java Microbenchmark Harness 微基准测试框架)进行性能测试和优化_霸道流氓气质的博客-CSDN博客

上面在使用JMH时测试了Java中数组和链表在进行头部插入时的对比结果。

下面分别对比在头部、中部、尾部分别进行查询和添加/删除时的性能对比,因添加和删除流程类似,

所以只测试添加即可。

Java中数组与链表

数组

数组(Array)是由相同类型的元素(element)的集合所组成的数据结构,分配⼀块连续的内存来存储。

利⽤元素的索引(index)可以计算出该元素对应的存储地址。

数组的“连续”特征决定了它的访问速度很快,因为它是连续存储的,所以这就决定了它的存储位置就是固定的,

因此它的访问速度就很快。

⽽缺点它对内存的要求⽐较⾼,必须要找到⼀块连续的内存才⾏。

数组的另⼀个缺点就是插⼊和删除的效率⽐较慢,假如我们在数组的⾮尾部插⼊或删除⼀个数据,那么就

要移动之后的所有数据,这就会带来⼀定的性能开销数组还有⼀个缺点,它的⼤⼩固定,不能动态拓展。

链表

链表(Linked list)是⼀种常⻅的基础数据结构,是⼀种线性表,但是并不会按线性的顺序存储数据,

⽽是在每⼀个节点⾥存到下⼀个节点的指针(Pointer)。由于不必须按顺序存储,链表在插⼊的时候可

以达到 O(1) 的复杂度,⽐另⼀种线性表顺序表快得多,但是查找⼀个节点或者访问特定编号的节点则

需要 O(n) 的时间,⽽顺序表相应的时间复杂度分别是 O(logn) 和 O(1)。

链表是⼀个⽆需连续内存存储的数据结构,链表的元素有两个属性,⼀个是元素的值,另⼀个是指针,

此指针标记了下⼀个元素的地址。

链表主要分为以下⼏类:

单向链表

单向链表中包含两个域,⼀个信息域和⼀个指针域。这个链接指向列表中的下⼀个节点,⽽最后⼀个节点则指向⼀个空值。

双向链表

双向链表也叫双链表,双向链表中不仅有指向后⼀个节点的指针,还有指向前⼀个节点的指针,这样可以

从任何⼀个节点访问前⼀个节点,当然也可以访问后⼀个节点,以⾄整个链表。

循环链表

循环链表中第⼀个节点之前就是最后⼀个节点,反之亦然。

为什么会有单、双链表之分?

这个就要从链表的删除说起了,如果单向链表要删除元素的话,不但要找到删除的节点,还要找到删除节

点的上⼀个节点(通常称之为前驱),因为需要变更上⼀个节点中 next 的指针,但⼜因为它是单向链表,

所以在删除的节点中并没有存储上⼀个节点的相关信息,那么我们就需要再查询⼀遍链表以找到上⼀个节点,

这样就带来了⼀定的性能问题,所以就有了双向链表。

链表的优点:

1. 链表对内存的利⽤率⽐较⾼,⽆需连续的内存空间,即使有内存碎⽚,也不影响链表的创建;

2. 链表的插⼊和删除的速度很快,⽆需像数组⼀样需要移动⼤量的元素;

3. 链表⼤⼩不固定,可以很⽅便的进⾏动态扩展。

链表缺点

链表的主要缺点是不能随机查找,必须从第⼀个开始遍历,查找效率⽐较低,链表查询的时间复杂度是O(n)。

在 Java 语⾔中,数组的代表为 ArrayList ,⽽链表的代表为 LinkedList ,因此我们就⽤这两个对象来进⾏测试。

注:

博客:
霸道流氓气质的博客_CSDN博客-C#,架构之路,SpringBoot领域博主

实现

1、测试头部添加

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 2,time = 1,timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5,time = 5,timeUnit = TimeUnit.SECONDS)
@Fork(1)
@State(Scope.Thread)
//头部添加性能测试
public class ArrayListWithLinkedListTestHeaderAdd {

    private static final int maxSize = 10000; //测试循环次数
    private static final int operationSize = 100; //操作次数

    private static ArrayList<Integer> arrayList;
    private static LinkedList<Integer> linkedList;

    public static void main(String[] args) throws RunnerException {
        //启动基准测试
        Options opt = new OptionsBuilder()
                .include(ArrayListWithLinkedListTestHeaderAdd.class.getSimpleName())  //要导入的测试类
                .build();
        //执行测试
        new Runner(opt).run();
    }

    //@Setup作用于方法上,用于测试前的初始化工作
    //启动执行事件
    @Setup
    public void init(){
        arrayList = new ArrayList<Integer>();
        linkedList = new LinkedList<Integer>();
        for (int i = 0; i < maxSize; i++) {
            arrayList.add(i);
            linkedList.add(i);
        }
    }

    //用于回收某些资源
    @TearDown
    public void finish(){

    }

    @Benchmark
    public void addArrayByFirst(Blackhole blackhole){
        for (int i = 0; i < operationSize; i++) {
            arrayList.add(i,i);
        }
        //为了避免JIT忽略未被使用的结果计算/为了避免死码消除问题
        blackhole.consume(arrayList);
    }

    @Benchmark
    public void addLinkedByFirst(Blackhole blackhole){
        for (int i = 0; i < operationSize; i++) {
            linkedList.add(i,i);
        }
        //为了避免JIT忽略未被使用的结果计算/为了避免死码消除问题
        blackhole.consume(linkedList);
    }
}

测试结果输出如下:

Benchmark                                              Mode  Cnt        Score         Error  Units
ArrayListWithLinkedListTestHeaderAdd.addArrayByFirst   avgt    5  6597432.461 ± 8384921.207  ns/op
ArrayListWithLinkedListTestHeaderAdd.addLinkedByFirst  avgt    5   111880.375 ±  258245.983  ns/op

可以看到在进行头部插入时LinkedList的平均完成时间比ArrayList平均完成时间快了约60倍

2、测试中部添加

    @Benchmark
    public void addArrayByMiddle(Blackhole blackhole){
        int startIndex = maxSize /2 ;//获取中间值
        for (int i = startIndex; i < (startIndex + operationSize); i++) {
            arrayList.add(i,i);
        }
        //为了避免JIT忽略未被使用的结果计算/为了避免死码消除问题
        blackhole.consume(arrayList);
    }

    @Benchmark
    public void addLinkedByMiddle(Blackhole blackhole){
        int startIndex = maxSize /2 ;//获取中间值
        for (int i = startIndex; i < (startIndex + operationSize); i++) {
            linkedList.add(i,i);
        }
        //为了避免JIT忽略未被使用的结果计算/为了避免死码消除问题
        blackhole.consume(linkedList);
    }

测试结果

Benchmark                                              Mode  Cnt        Score         Error  Units
ArrayListWithLinkedListTestMiddleAdd.addArrayByMiddle   avgt    5  6703786.087 ± 9225973.854  ns/op
ArrayListWithLinkedListTestMiddleAdd.addLinkedByMiddle  avgt    5  1064823.250 ±  697306.502  ns/op

可以看到在进行中间插入时LinkedList的平均完成时间比ArrayList平均完成时间快了约7倍

3、测试尾部添加

    @Benchmark
    public void addArrayByTail(Blackhole blackhole){
        int startIndex = maxSize - 1 - operationSize ;
        for (int i = startIndex; i < (maxSize - 1); i++) {
            arrayList.add(i,i);
        }
        //为了避免JIT忽略未被使用的结果计算/为了避免死码消除问题
        blackhole.consume(arrayList);
    }

    @Benchmark
    public void addLinkedByTail(Blackhole blackhole){
        int startIndex = maxSize - 1 - operationSize ;
        for (int i = startIndex; i < (maxSize - 1); i++) {
            linkedList.add(i,i);
        }
        //为了避免JIT忽略未被使用的结果计算/为了避免死码消除问题
        blackhole.consume(linkedList);
    }

测试结果

Benchmark                                            Mode  Cnt        Score          Error  Units
ArrayListWithLinkedListTestTailAdd.addArrayByTail   avgt    5  6981011.770 ± 10173306.725  ns/op
ArrayListWithLinkedListTestTailAdd.addLinkedByTail  avgt    5  2180224.087 ±   913796.230  ns/op

可以看到在进行尾部插入时LinkedList的平均完成时间比ArrayList平均完成时间快了约3倍

4、测试头部查询

    @Benchmark
    public void searchArrayByFirst(){
        for (int i = 0; i < operationSize; i++) {
            arrayList.get(i);
        }
    }

    @Benchmark
    public void searchLinkedByFirst(){
        for (int i = 0; i < operationSize; i++) {
            linkedList.get(i);
        }
    }

测试结果

Benchmark                                                    Mode  Cnt     Score      Error  Units
ArrayListWithLinkedListTestHeaderSearch.searchArrayByFirst   avgt    5     3.578 ±    0.835  ns/op
ArrayListWithLinkedListTestHeaderSearch.searchLinkedByFirst  avgt    5  4232.832 ± 2019.900  ns/op

5、测试中部查询

    @Benchmark
    public void searchArrayByMiddle(){
        int startIndex = maxSize / 2;
        int endIndex = startIndex + operationSize;
        for (int i = startIndex; i < endIndex; i++) {
            arrayList.get(i);
        }
    }

    @Benchmark
    public void searchLinkedByMiddle(){
        int startIndex = maxSize / 2;
        int endIndex = startIndex + operationSize;
        for (int i = startIndex; i < endIndex; i++) {
            linkedList.get(i);
        }
    }

测试结果

Benchmark                                                     Mode  Cnt        Score        Error  Units
ArrayListWithLinkedListTestMiddleSearch.searchArrayByMiddle   avgt    5        4.969 ±      6.568  ns/op
ArrayListWithLinkedListTestMiddleSearch.searchLinkedByMiddle  avgt    5  1131641.772 ± 313408.389  ns/op

可以看到在进行中部查询时ArrayList的平均完成时间比LinkedList平均完成时间快了约230947倍

6、测试尾部查询

    @Benchmark
    public void searchArrayByTail(){
        for (int i = maxSize - operationSize; i < maxSize; i++) {
            arrayList.get(i);
        }
    }

    @Benchmark
    public void searchLinkedByTail(){
        for (int i = maxSize - operationSize; i < maxSize; i++) {
            linkedList.get(i);
        }
    }

测试结果

Benchmark                                                 Mode  Cnt     Score     Error  Units
ArrayListWithLinkedListTestTailSearch.searchArrayByTail   avgt    5     5.074 ±   5.384  ns/op
ArrayListWithLinkedListTestTailSearch.searchLinkedByTail  avgt    5  5689.329 ± 563.806  ns/op

可以看到在进行尾部查询时ArrayList的平均完成时间比LinkedList平均完成时间快了约1100倍

7、总结:Java中是否是否链表比数组慢了1000多倍?

可以看到在进行头部查询时ArrayList的平均完成时间比LinkedList平均完成时间快了约1410倍

我们在最后的评测中可以看出,当数据初始化完成之后,我们再进⾏插⼊操作时,尤其是从头部插⼊时,

因为数组要移动之后的所有元素,因此性能要⽐链表低很多;但在查询时性能刚好相反,因为链表要遍历查询,

并且LinkedList 是双向链表,所以在中间查询时性能要⽐数组查询慢了上万倍(查询 100 个元素),

⽽两头查询(⾸部和尾部)时,链表也⽐数组慢了将近 1000 多倍(查询 100 个元素)。

因此在查询⽐较多的场景中,我们要尽量使⽤数组,⽽在添加和删除操作⽐较多时,我们应该使⽤链表结构。

猜你喜欢

转载自blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/131750105