列表、原始类型和性能

3

我对自动装箱和性能感到好奇,因为我在我的应用程序中进行了大量与速度有关的数学计算,所以我进行了一些简单的测试...

public static void main(String[] args) {
    // Some initialization so I know it's not involved
    ArrayList<Integer> list = new ArrayList<Integer>();
    list.add(0);
    int[] regArray = new int[1];
    long total = 0;

    // This one uses an array and primitive type
    long start = System.currentTimeMillis();
    for (int i = 0; i < 10000000; i++) {
        regArray[0] = i + 10;
        if (regArray[0] % 1000 == 0) total += regArray[0];
    }
    System.out.println("Runtime in millis: " + (System.currentTimeMillis() - start));
    System.out.println(total);

    // This one autoboxes, but still uses the Object type because it's a list
    total = 0;
    start = System.currentTimeMillis();
    for (int i = 0; i < 10000000; i++) {
        list.set(0, i + 10);
        if (list.get(0) % 1000 == 0) total += list.get(0);
    }
    System.out.println("Runtime in millis: " + (System.currentTimeMillis() - start));
    System.out.println(total);

    // This one doesn't autobox
    total = 0;
    start = System.currentTimeMillis();
    for (int i = 0; i < 10000000; i++) {
        list.set(0, new Integer(i + 10));
        if (list.get(0).intValue() % 1000 == 0) total += list.get(0).intValue();
    }
    System.out.println("Runtime in millis: " + (System.currentTimeMillis() - start));
    System.out.println(total);
}

这是一个样例输出:
Runtime in millis: 78
50005000000
Runtime in millis: 250
50005000000
Runtime in millis: 250
50005000000

这似乎表明在对数学和速度敏感的应用程序中,我应该避免使用 List<> 和其子类。您同意吗,stackoverflow?
编辑:我的实际用例是需要存储几百个经常变化且难以预测的 intfloat(我之所以说难以预测,是因为它们将保持在一个较窄的范围内,但我不知道它们在那个狭窄的范围内会做什么),并且需要毫秒级响应时间来处理这些数字的计算。

你的基准测试存在缺陷,因为你每次都在同一个JVM调用实例中运行。因此后续的测试会受到JVM预热的好处。至少,你需要在每个测试中使用独立的JVM来运行。如果你重新排序测试,你将得到不同的结果。 - Steve Kuo
3个回答

3

微基准测试很难!我重写了您的基准测试,使用caliper

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

import java.util.ArrayList;

public class ListsBenchmark extends SimpleBenchmark {

    private final ArrayList<Integer> list = new ArrayList<Integer>();
    int[] regArray = new int[1];
    long total;

    @Override
    protected void setUp() throws Exception {
        list.add(0);
        total = 0;
    }

    public long timeArrayAndPrimitiveType(int reps) {
        for (int i = 0; i < reps; i++) {
            regArray[0] = i + 10;
            if (regArray[0] % 1000 == 0)
                total += regArray[0];
        }
        return total;
    }

    public long timeListWithAutoboxing(int reps) {
        for (int i = 0; i < reps; i++) {
            list.set(0, i + 10);
            if (list.get(0) % 1000 == 0)
                total += list.get(0);
        }
        return total;
    }

    public long timeNoAutoboxing(int reps) {
        for (int i = 0; i < reps; i++) {
            list.set(0, new Integer(i + 10));
            if (list.get(0).intValue() % 1000 == 0)
                total += list.get(0).intValue();
        }
        return total;
    }

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

}

我没有修改您的原始代码。我发现以下内容:

  • 数组大约快3倍
  • 创建新的整数(Integer)稍微更快!可能缓存有一些代价,或者这只是我的架构(32位Ubuntu, 4核和3 GiB内存的笔记本电脑)。

在图表上(可以随意运行!):

Caliper


这太棒了!我感到很有动力,要用自定义的基于数组的数据缓存重写我的代码,并自己实现 java.util.List 的便利操作... 不使用泛型,因为我现在知道并且将来也知道数据类型是什么。 - durron597

2
如果您有一个预定数量的整数,那么将它们存储在数组中(假设它们需要存储且无法流式传输!)通常比使用java.util.ArrayList更快,是的。
然而,在许多情况下,您可能具有可变的数据大小,因此动态可调整大小的集合变得非常有用 - 另一种选择通常是编写自己的ArrayList实现!
幸运的是,有许多第三方库基于原始类型(int等)而不是对象(Integer等)来实现List。 您可以查看这些库。

好的回答,DNA:如果您对我特定情况有好的建议,我已经在我的问题中添加了更多关于我的用例的内容。 - durron597

1

编写基准测试是一项艰巨的任务,而您的基准测试并不好,至少有以下原因:

  • 在主方法中执行所有操作,而不让Hotspot进入和JIT编译您的代码会导致结果错误
  • 使用new Integer()而不是Integer.valueOf()将无法使用整数缓存
  • 值和操作不够现实。大多数情况下,值接近0,而不是1000万范围内的值

通常,原始类型和数组比对象和集合更快,但如果没有在实际条件下测量您的实际代码,很难确定使用原始类型和数组所获得的收益是否显著或完全可以忽略不计。大多数情况下,性能损失发生在IO操作中。


在第一个List的情况下,我让自动装箱处理它。你是说如果我循环遍历同样的1000个整数,那么它会更加可比吗?另外,你能解释一下你所说的Hotspot和JIT吗? - durron597
Hotspot是Oracle JVM的即时编译器。它分析哪些方法经常被执行(热点),并将它们编译成本地代码以加速它们。据我所知,将所有内容放在主方法中会使Hotspot无法发挥作用。此外,应该多次调用这些方法,以使JIT将其检测为热点,然后再次调用以测量其执行时间。 - JB Nizet
谢谢,我从这篇文章中学到了很多,尽管它并没有真正回答我的问题。我已经在我的帖子中添加了一个使用案例。 - durron597

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接