Java ArrayList与OpenArrayList性能

时间:2020-01-09 10:36:17  来源:igfitidea点击:

Java应用程序经常将对象保存在包含" java.util.ArrayList"实例的数据结构中。当迭代那些数据结构中的对象时,我们还必须迭代存储在ArrayList实例中的对象。在此Java ArrayList性能教程中,我将仔细研究迭代" ArrayList"的不同方法的性能。本教程还将研究" OpenArrayList"类的性能,该类模仿" java.util.ArrayList",但在设计时会考虑性能。

迭代ArrayList的三种方法

基本上有三种不同的方法可以迭代" ArrayList"中包含的对象:

  • 使用for循环。
  • 使用for-each循环。
  • 使用迭代器。

这三种迭代ArrayList的方式之间并没有很大的性能差异,但是有一点,而且足够大,如果代码对性能至关重要,那么我们可能希望获得这种几乎免费的性能增益。但是首先,让我向我们展示迭代" ArrayList"的三种方法。

for循环

使用标准的Javafor循环迭代ArrayList看起来像这样:

ArrayList arrayList = ...//get ArrayList from somewhere

long sum = 0;    
for(int i=0, n=arrayList.size(); i < n; i++){
    Object element = arrayList.get(i);
}

如我们所见," for"循环使用标准计数器来对存储在" ArrayList"中的所有元素进行计数。每个元素都是使用get()方法从ArrayList实例获得的。

每个循环

for-each循环使用Java 5中添加的for构造。这是使用for-each循环迭代ArrayList的样子:

ArrayList arrayList = ...//get ArrayList from somewhere

long sum = 0;
for(Object obj : arrayList){

}

迭代器

迭代" ArrayList"的第三种方法是使用从" ArrayList"获得的" java.util.Iterator"。这是使用Iterator迭代ArrayList的样子:

ArrayList arrayList = ...//get ArrayList from somewhere

long sum = 0;
Iterator iterator = arrayList.iterator();
while(iterator.hasNext()) {
    Object element = iterator.next();
}

ArrayList迭代基准

为了测量三种不同的迭代" ArrayList"的迭代性能差异,我使用Java Microbenchmark Harness实现了三种不同的基准测试方法。这些基准测试的代码包含在本文的结尾。

为了更准确地了解每种技术的迭代速度,我测量了迭代包含10个元素和100个元素的" ArrayList"的速度。这样,我相信我会对性能有更细致的了解。

ArrayList中的元素是Long元素,它们相加。因此,基准测试实际上测量了存储在" ArrayList"中的10个和100个" Long"元素的迭代和求和。

基准测试是在Intel Core i7-4770 Haswell服务器上使用JDK 1.8.0_u60执行的,该服务器仅执行基准测试。使用JMH默认值执行基准测试,这意味着20次预热迭代,20次迭代和10次fork。以下是基准测试结果(JMH的输出):

Benchmark                                                Mode  Cnt         Score        Error  Units
OpenArrayListBenchmark.jdkArrayList100SumUsingGet       thrpt  200  15838998.503 ±   1017.752  ops/s
OpenArrayListBenchmark.jdkArrayList100SumUsingForEach   thrpt  200  14068898.854 ±    946.976  ops/s
OpenArrayListBenchmark.jdkArrayList100SumUsingIterator  thrpt  200  14069990.330 ±    512.600  ops/s
OpenArrayListBenchmark.jdkArrayList10SumUsingGet        thrpt  200  77320532.538 ±   7421.267  ops/s
OpenArrayListBenchmark.jdkArrayList10SumUsingForEach    thrpt  200  69926532.927 ± 732112.779  ops/s
OpenArrayListBenchmark.jdkArrayList10SumUsingIterator   thrpt  200  69879781.630 ± 727551.844  ops/s

如我们所见,使用for-each循环和Iterator迭代ArrayList产生的性能几乎相同。这是预料之中的,因为Java编译器可能使用Iteratorfor-each循环编译成一个迭代。

我们还可以看到,使用带有计数器的标准Javafor循环来迭代" ArrayList",并通过调用" ArrayList"的get()方法获取每个元素,对于具有10的" ArrayList"而言,速度要快大约10%。元素,当ArrayList包含100个元素时,速度提高约12,5%。

究竟为什么会有这样的性能差异是很难说出来的。部分差异可能是由于每次迭代创建一个"迭代器"对象引起的。但是,我们希望当" ArrayList"包含更多元素时,开销会被平分(不太明显)。但是,相反,性能差异似乎有所增加(从10个元素的10%增长到100个元素的12.5%)。这可能是由于CPU对for循环版本进行了更优化的循环执行而引起的,但是我不能肯定地说。

OpenArrayList

OpenArrayList类是对ArrayList的非常简单的模仿,我已经实现了它,以查看它是否可以比ArrayList更快地迭代元素集合。这是OpenArrayList实现的抓取版本:

public Object[] elements = null;

    public int capacity = 0;
    public int size     = 0;

    public OpenArrayList(){

    }

    public OpenArrayList(int capacity){
        this.capacity = capacity;
        this.elements = new Object[capacity];
    }

    public OpenArrayList(Object[] elements){
        this.elements = elements;
        this.capacity = elements.length;
    }

    public void add(Object element){
        this.elements[this.size++] = element;
    }

    public boolean addIfCapacity(Object element){
        if(this.size < this.capacity){
            this.elements[this.size++] = element;
            return true;
        }
        return false;
    }
}

首先要注意的是,所有" OpenArrayList"成员变量都是公共的。这就是为什么我称其为"开放"。它的成员变量对外界开放。我已经公开了成员变量,以便在迭代OpenArrayList中的元素时可以直接访问elements数组。这应该比调用ArrayListget()方法要快一点,尽管JVM可以优化掉get()方法的调用。

公开元素数组的另一个好处是,我们可以使用System.arraycopy()对其进行写入或者复制,这非常快。

OpenArrayList迭代基准

与" ArrayList"一样,我测量了存储在" OpenArrayList"中的10个对象和100个"长"对象的总和。使用与" ArrayList"基准相同的设置执行基准。

以下是OpenArrayList迭代基准测试(下面带有ArrayList基准测试以便于比较):

Benchmark                                                Mode  Cnt         Score        Error  Units
OpenArrayListBenchmark.openArrayList100Sum              thrpt  200  16040305.970 ±   1490.153  ops/s
OpenArrayListBenchmark.openArrayList10Sum               thrpt  200  81301297.431 ±  15104.301  ops/s

OpenArrayListBenchmark.jdkArrayList100SumUsingGet       thrpt  200  15838998.503 ±   1017.752  ops/s
OpenArrayListBenchmark.jdkArrayList100SumUsingForEach   thrpt  200  14068898.854 ±    946.976  ops/s
OpenArrayListBenchmark.jdkArrayList100SumUsingIterator  thrpt  200  14069990.330 ±    512.600  ops/s
OpenArrayListBenchmark.jdkArrayList10SumUsingGet        thrpt  200  77320532.538 ±   7421.267  ops/s
OpenArrayListBenchmark.jdkArrayList10SumUsingForEach    thrpt  200  69926532.927 ± 732112.779  ops/s
OpenArrayListBenchmark.jdkArrayList10SumUsingIterator   thrpt  200  69879781.630 ± 727551.844  ops/s

如我们所见,当" ArrayList"包含100个元素时," OpenArrayList"迭代速度快约1%,而包含10个元素的迭代速度快约5%。确切地说,为什么存在性能差异,我不能肯定地说。性能如此接近的事实可能表明JVM优化了get()调用。但是,仍然有趣的是,对于较少数量的元素,似乎存在较大的性能差异。

迭代基准代码

这是用于测量ArrayList和OpenArrayList的不同迭代技术的迭代速度的基准代码。

public class OpenArrayListBenchmark {

    @State(Scope.Thread)
    public static class BenchmarkState {

        public ArrayList<Long> jdkArrayList10 = new ArrayList<>();
        public ArrayList<Long> jdkArrayList100 = new ArrayList<>();
        public OpenArrayList openArrayList10 = new OpenArrayList(10);
        public OpenArrayList openArrayList100 = new OpenArrayList(100);

        @Setup(Level.Trial)
        public void toSetup() {
            Object[] elements = openArrayList10.elements;
            for(int i=0; i < openArrayList10.capacity; i++){
                elements[i] = new Long(i);
                jdkArrayList10.add(new Long(i));
            }
            openArrayList10.size = openArrayList10.capacity;
    
            elements = openArrayList100.elements;
            for(int i=0; i < openArrayList100.capacity; i++){
                elements[i] = new Long(i);
                jdkArrayList100.add(new Long(i));
            }
            openArrayList100.size = openArrayList100.capacity;
        }

    }

    @Benchmark
    @BenchmarkMode(Mode.Throughput)
    public long openArrayList10Sum(BenchmarkState state, Blackhole blackhole) {

        long sum = 0;
        Object[] elements = state.openArrayList10.elements;
        for(int i=0; i<state.openArrayList10.size; i++){
            sum += ((Long) elements[i]).longValue();
        }
    
        blackhole.consume(sum);
    
        return sum;
    }

    @Benchmark
    @BenchmarkMode(Mode.Throughput)
    public long openArrayList100Sum(BenchmarkState state, Blackhole blackhole) {

        long sum = 0;
        Object[] elements = state.openArrayList100.elements;
        for(int i=0; i<state.openArrayList100.size; i++){
            sum += ((Long) elements[i]).longValue();
        }
    
        blackhole.consume(sum);

        return sum;
    }

    @Benchmark
    @BenchmarkMode(Mode.Throughput)
    public long jdkArrayList10SumUsingGet(BenchmarkState state, Blackhole blackhole) {
    
        long sum = 0;
        ArrayList arrayList = state.jdkArrayList10;
        for(int i=0, n=state.jdkArrayList10.size(); i < n; i++){
            sum += ((Long) arrayList.get(i)).longValue();
        }
    
        blackhole.consume(sum);
    
        return sum;
    }

    @Benchmark
    @BenchmarkMode(Mode.Throughput)
    public long jdkArrayList100SumUsingGet(BenchmarkState state, Blackhole blackhole) {

        long sum = 0;
        ArrayList arrayList = state.jdkArrayList100;
        for(int i=0, n=state.jdkArrayList100.size(); i < n; i++){
            sum += ((Long) arrayList.get(i)).longValue();
        }
    
        blackhole.consume(sum);
    
        return sum;
    }

    @Benchmark
    @BenchmarkMode(Mode.Throughput)
    public long jdkArrayList10SumUsingForEach(BenchmarkState state, Blackhole blackhole) {
    
        long sum = 0;
    
        for(Long element : state.jdkArrayList10){
            sum += element.longValue();
        }
    
        blackhole.consume(sum);
    
        return sum;
    }

    @Benchmark
    @BenchmarkMode(Mode.Throughput)
    public long jdkArrayList100SumUsingForEach(BenchmarkState state, Blackhole blackhole) {

        long sum = 0;
    
        for(Long element : state.jdkArrayList100){
            sum += element.longValue();
        }
    
        blackhole.consume(sum);
    
        return sum;
    }

    @Benchmark
    @BenchmarkMode(Mode.Throughput)
    public long jdkArrayList10SumUsingIterator(BenchmarkState state, Blackhole blackhole) {

    long sum = 0;
    Iterator<Long> iterator = state.jdkArrayList10.iterator();
        while(iterator.hasNext()){
            sum += iterator.next().longValue();
        }

        blackhole.consume(sum);

        return sum;
    }

    @Benchmark
    @BenchmarkMode(Mode.Throughput)
    public long jdkArrayList100SumUsingIterator(BenchmarkState state, Blackhole blackhole) {

        long sum = 0;
        Iterator<Long> iterator = state.jdkArrayList100.iterator();
        while(iterator.hasNext()){
            sum += iterator.next().longValue();
        }

        blackhole.consume(sum);

        return sum;
    }
}