【问题标题】:Are ArrayLists more than twice as slow as arrays?ArrayLists 的速度是数组的两倍多吗?
【发布时间】:2013-12-27 05:55:55
【问题描述】:

我写了一个测试,试图测试两件事:

  • 缓冲区数组的大小是否会影响其性能,即使您不使用整个缓冲区也是如此
  • 数组和ArrayList的相对性能

我对结果有点惊讶

  • 盒装数组(即Integer vs int)并不比原始版本慢很多
  • 底层数组的大小并不重要
  • ArrayLists 的速度是对应数组的两倍多。

问题

  1. 为什么ArrayList 这么慢?
  2. 我的基准测试写得好吗?换句话说,我的结果准确吗?

结果

 0% Scenario{vm=java, trial=0, benchmark=SmallArray} 34.57 ns; ?=0.79 ns @ 10 trials
17% Scenario{vm=java, trial=0, benchmark=SmallBoxed} 40.40 ns; ?=0.21 ns @ 3 trials
33% Scenario{vm=java, trial=0, benchmark=SmallList} 105.78 ns; ?=0.09 ns @ 3 trials
50% Scenario{vm=java, trial=0, benchmark=BigArray} 34.53 ns; ?=0.05 ns @ 3 trials
67% Scenario{vm=java, trial=0, benchmark=BigBoxed} 40.09 ns; ?=0.23 ns @ 3 trials
83% Scenario{vm=java, trial=0, benchmark=BigList} 105.91 ns; ?=0.14 ns @ 3 trials

 benchmark    ns linear runtime
SmallArray  34.6 =========
SmallBoxed  40.4 ===========
 SmallList 105.8 =============================
  BigArray  34.5 =========
  BigBoxed  40.1 ===========
   BigList 105.9 ==============================

vm: java
trial: 0

代码

这段代码是在 Windows 中使用 Java 7 和 Google caliper 0.5-rc1 编写的(因为上次我检查了 1.0 还不能在 Windows 中运行)。

简要概述:在所有 6 次测试中,在循环的每次迭代中,它将数组的前 128 个单元格中的值相加(无论数组有多大),并将其添加到总值中。 Caliper 告诉我测试应该运行多少次,所以我循环添加了 128 次。

这 6 个测试有 int[]Integer[]ArrayList<Integer> 的大 (131072) 和小 (128) 版本。你大概可以从名字中找出哪个是哪个。

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import com.google.caliper.Runner;
import com.google.caliper.SimpleBenchmark;

public class SpeedTest {    
    public static class TestBenchmark extends SimpleBenchmark {
        int[] bigArray = new int[131072];
        int[] smallArray = new int[128];
        Integer[] bigBoxed = new Integer[131072];
        Integer[] smallBoxed = new Integer[128];
        List<Integer> bigList = new ArrayList<>(131072);
        List<Integer> smallList = new ArrayList<>(128);

        @Override
        protected void setUp() {
            Random r = new Random();
            for(int i = 0; i < 128; i++) {
                smallArray[i] = Math.abs(r.nextInt(100));
                bigArray[i] = smallArray[i];
                smallBoxed[i] = smallArray[i];
                bigBoxed[i] = smallArray[i];
                smallList.add(smallArray[i]);
                bigList.add(smallArray[i]);
            }
        }

        public long timeBigArray(int reps) {
            long result = 0;
            for(int i = 0; i < reps; i++) {
                for(int j = 0; j < 128; j++) {
                    result += bigArray[j];
                }
            }
            return result;
        }

        public long timeSmallArray(int reps) {
            long result = 0;
            for(int i = 0; i < reps; i++) {
                for(int j = 0; j < 128; j++) {
                    result += smallArray[j];
                }
            }
            return result;
        }

        public long timeBigBoxed(int reps) {
            long result = 0;
            for(int i = 0; i < reps; i++) {
                for(int j = 0; j < 128; j++) {
                    result += bigBoxed[j];
                }
            }
            return result;
        }

        public long timeSmallBoxed(int reps) {
            long result = 0;
            for(int i = 0; i < reps; i++) {
                for(int j = 0; j < 128; j++) {
                    result += smallBoxed[j];
                }
            }
            return result;
        }

        public long timeBigList(int reps) {
            long result = 0;
            for(int i = 0; i < reps; i++) {
                for(int j = 0; j < 128; j++) {
                    result += bigList.get(j);
                }
            }
            return result;
        }

        public long timeSmallList(int reps) {
            long result = 0;
            for(int i = 0; i < reps; i++) {
                for(int j = 0; j < 128; j++) {
                    result += smallList.get(j);
                }
            }
            return result;
        }
    }

    public static void main(String[] args) {
        Runner.main(TestBenchmark.class, new String[0]);
    }
}

【问题讨论】:

  • 那个运行时的运行时是什么?
  • @SamIam 我不确定我是否理解这个问题;运行时是它下面的基准代码。
  • 是整个timeBigBoxed 或类似方法所花费的时间吗?这是for 循环之一的单次迭代所花费的时间吗?
  • Random.nextInt(n) 总是返回一个正数。 BTW Math.abs(n) 总是返回一个正数。
  • @PeterLawrey 实际上 Random.nextInt(n) 返回一个 非负 数,但您的观点是,在结果上调用 Math.abs() 毫无意义。无论如何+1。 :-)

标签: java arrays performance arraylist


【解决方案1】:

请记住,在使用 ArrayList 时,您实际上是在调用一个函数,在 get() 的情况下实际上调用了另外两个函数。 (其中之一是范围检查,我怀疑这可能是延迟的一部分)。

ArrayList 的重要之处不是它与直接数组相比快多少或慢多少,而是它的访问时间总是恒定的(就像数组一样)。在现实世界中,您几乎总是会发现增加的延迟可以忽略不计。特别是如果您有一个甚至考虑连接到数据库的应用程序。 :)

简而言之,我认为您的测试(和结果)是合法的。

【讨论】:

  • 我认为您对范围检查是正确的。而ArrayList 必须检查两个不同的范围,因为即使支持数组的长度为 100 个元素,如果有效大小仅为 3,则不能仅依赖数组边界。
  • 正确 - 这是它必须进行检查的唯一真正原因。否则,它可能只依赖数组访问来抛出 IndexOutOfBoundsException
  • 编译器会优化方法调用,这样调用本身的成本就不高了,但重要的是调用 get() 会导致 2 个动作而不是 1 个动作。范围检查很可能是导致性能下降的原因。
  • ArrayList 的方法很可能会被内联
【解决方案2】:

这些结果并不让我感到惊讶。 List.get(int) 涉及演员阵容,这很慢。 Java 的泛型是通过类型擦除实现的,这意味着List&lt;T&gt; 的任何类型实际上都是List&lt;Object&gt;,而你得到你的类型的唯一原因是因为强制转换。 ArrayList 的源代码如下所示:

public E get(int index) {
    rangeCheck(index);

    return elementData(index);
}
// snip...
private void rangeCheck(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
// snip...
@SuppressWarnings("unchecked")
E elementData(int index) {
    return (E) elementData[index];
}

rangeCheck 和函数调用的开销是微不足道的,只是转换为 E 会害死你。

【讨论】:

  • 演员相当便宜。上面代码中对 (E) 的强制转换完全是虚构的。
  • 您知道有没有表明类型转换特别慢的消息来源?
  • @HotLicks 实际上他对代码是正确的。 ArrayListObject[] 而非 E[] 支持。我不确定的是演员阵容是否是真正的问题。我认为它更有可能是rangeCheck
  • @durron597 - 但我坚持我的说法,即上述演员阵容是虚构的。真正的演员发生在调用方。
  • 我什至不确定这个特定的转换(因为它是转换为参数化类型 - 不是实际类型)甚至在字节码级别有什么作用吗? (与其说是陈述,不如说是一个问题。)
【解决方案3】:

首先...

ArrayList 的速度是数组的两倍多吗?

一般来说,没有。对于可能涉及“更改”列表/数组长度的操作,ArrayList 将比数组快...除非您使用单独的变量来表示数组的逻辑大小。

对于其他操作,ArrayList 可能会更慢,尽管性能比率很可能取决于操作和 JVM 实现。另请注意,您只测试了一种操作/模式。

为什么 ArrayList 这么慢?

因为 ArrayList 内部有一个不同的数组对象。

  • 操作通常涉及额外的间接操作(例如,获取列表的大小和内部数组)并且有额外的边界检查(例如,检查列表的 size 和数组的长度)。典型的 JIT 编译器(显然)无法优化这些。 (事实上​​,您不希望优化内部数组,因为这是允许 ArrayList 增长的原因。)

  • 对于原始数组,相应的列表类型涉及包装的原始类型/对象,这会增加开销。例如,您的 result += ... 在“列表”情况下涉及拆箱。

我的基准测试写得好吗?换句话说,我的结果准确吗?

技术上没有问题。但这还不足以证明你的观点。首先,您只测量一种操作:数组元素获取及其等价物。而且您只测量原始类型。


最后,这在很大程度上忽略了使用List 类型的意义。我们使用它们是因为它们几乎总是比普通数组更易于使用。 (比如说)2 的性能差异通常对整体应用程序性能并不重要。

【讨论】:

  • 很好的答案@StephenC。不过,我会说,在我的用例中(我编写基准测试的原因),该操作是最重要的;插入和删除之类的事情远没有那么重要。
  • 很公平。但是你不应该试图以你的问题文本的方式概括......。
【解决方案4】:

如果您存储数百万个对象,那么 Add 或 Contains 函数将会非常慢。最好的方法是使用数组的 hashMap 来拆分它。虽然类似的算法可以用于其他类型的对象,但这就是我如何将 1000 万个字符串的处理速度提高了 1000 倍(占用的内存是 2-3 倍)

public static class ArrayHashList  {
    private String temp1, temp2;
    HashMap allKeys = new HashMap();
    ArrayList curKeys;  
    private int keySize;
    public ArrayHashList(int keySize) {
        this.keySize = keySize;
    }   
    public ArrayHashList(int keySize, String fromFileName) {
        this.keySize = keySize;
        String line;
        try{
            BufferedReader br1 = new BufferedReader(new FileReader(fromFileName));        
            while ((line = br1.readLine()) != null) 
                addString(line);
            br1.close();
        }catch(Exception e){
            e.printStackTrace();
        }
    }   
    public boolean addString(String strToAdd) {  
        if (strToAdd.length()<keySize)
            temp1 = strToAdd;
        else
            temp1 = strToAdd.substring(0,keySize);
        if (!allKeys.containsKey(temp1))
            allKeys.put(temp1,new ArrayList());
        curKeys =  (ArrayList)allKeys.get(temp1);
        if (!curKeys.contains(strToAdd)){
            curKeys.add(strToAdd);
            return true;
        }
        return false;
    }
    public boolean haveString(String strCheck) { 
        if (strCheck.length()<keySize)
            temp1 = strCheck;
        else
            temp1 = strCheck.substring(0,keySize);
        if (!allKeys.containsKey(temp1))
            allKeys.put(temp1,new ArrayList());
        curKeys =  (ArrayList)allKeys.get(temp1);
        return curKeys.contains(strCheck);
    }
}

初始化并使用它:

ArrayHashList fullHlist = new ArrayHashList(3, filesPath+"\\allPhrases.txt");       
ArrayList pendingList = new ArrayList();
BufferedReader br1 = new BufferedReader(new FileReader(filesPath + "\\processedPhrases.txt"));
while ((line = br1.readLine()) != null) {
    wordEnc = StrUtil.GetFirstToken(line,",~~~,");
    if (!fullHlist.haveString(wordEnc))
        pendingList.add(wordEnc);
}
br1.close();    

【讨论】:

  • 这回答了一个不同的问题。我的问题是“为什么?”,您的回答是“好的,那现在怎么办”?
猜你喜欢
  • 1970-01-01
  • 2017-07-16
  • 2021-07-02
  • 2011-06-16
  • 2011-01-18
  • 1970-01-01
  • 1970-01-01
  • 2013-06-24
  • 1970-01-01
相关资源
最近更新 更多