如何正确使用JMH? ArrayList 示例

问题描述

在我的示例中,理论上两种方法性能应该非常相似。在第一种情况下,我使用数组,在第二种情况下 - ArrayList 具有保证的容量。

结果如下:

LessonBenchmark2.capacityTestArray avgt 5 1,354 ± 0,057 毫秒/操作

LessonBenchmark2.capacityTestArrayListEnsured 平均 5 32,018 ± 81,911 毫秒/操作

这里似乎数组要快得多(1.354 vs 32.018 ms/op)。可能是我的 JMH 基准测试设置不正确。如何做到正确?

此外,如果我使用 @Setup(Level.Invocation),那么结果很接近(1,405 对 1,496 ms/op):

LessonBenchmark.capacityTestArray 平均 5 1,405 ± 0,143 毫秒/操作

LessonBenchmark.capacityTestArrayListEnsured avgt 5 1,496 ± 0,104 毫秒/操作

不过据说要小心使用 Invocation。此外,迭代模式在逻辑上似乎是正确的。

代码如下:

public static void main(String[] args) throws Exception {
    org.openjdk.jmh.Main.main(args);
}

static final int iter = 5;
static final int fork = 1;
static final int warmIter = 5;

@State(Scope.Benchmark)
public static class Params {
    public int length = 100_000;
    public Person[] people;
    public ArrayList<Person> peopleArrayListEnsure;

    // before each iteration of the benchmark
    @Setup(Level.Iteration)
    public void setup() {
        people = new Person[length];
        peopleArrayListEnsure = new ArrayList<>(length);
    }
}

@Benchmark
@Warmup(iterations = warmIter)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Fork(value = fork)
@Measurement(iterations = iter)
public void capacityTestArray(Params p) {
    for (int i = 0; i < p.length; i++) {
        p.people[i] = new Person(i,new Address(i,i),new Pet(i,i));
    }
}

@Benchmark
@Warmup(iterations = warmIter)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Fork(value = fork)
@Measurement(iterations = iter)
public void capacityTestArrayListEnsured(Params p) {
    for (int i = 0; i < p.length; i++) {
        p.peopleArrayListEnsure.add(new Person(i,i)));
    }
}

public static class Person {
    private int id;
    private Address address;
    private Pet pet;

    public Person(int id,Address address,Pet pet) {
        this.id = id;
        this.address = address;
        this.pet = pet;
    }
}

public static class Address {
    private int countryId;
    private int cityId;

    public Address(int countryId,int cityId) {
        this.countryId = countryId;
        this.cityId = cityId;
    }
}

public static class Pet {
    private int age;
    private int typeId;

    public Pet(int age,int typeId) {
        this.age = age;
        this.typeId = typeId;
    }
}

解决方法

测试设计不当;在您的测试中,因为arraylist 只为多次调用创建一次,基于数组的代码只是多次覆盖同一个数组,而arraylist 版本添加的越来越多,需要增长。

一个简单的解决方法是先清除它。另一个解决方法是在此处停止使用状态,而将对象的创建(无论是 100k 人数组,还是预定义为 100k 人的人员数组列表)作为测试工具的一部分。一旦你解决了这个问题,考虑到错误,结果是完全相同的,对于这个,数组和数组列表之间的性能根本没有

MyBenchmark.capacityTestArray             avgt    5  1,325 ± 0,059  ms/op
MyBenchmark.capacityTestArrayListEnsured  avgt    5  1,287 ± 0,157  ms/op

我通过完全删除 Params 状态进行简化,并将列表和数组的创建作为每个测试的支出的一部分:

    static final int LEN = 100_000;
    
    public void capacityTestArray() {
        Person[] people = new Person[LEN];
        for (int i = 0; i < LEN; i++) {
            people[i] = new Person(i,new Address(i,i),new Pet(i,i));
        }
    }

    public void capacityTestArrayListEnsured() {
        List<Person> p = new ArrayList<Person>(LEN);
        for (int i = 0; i < LEN; i++) {
            p.add(new Person(i,i)));
        }
    }

(保持所有注释和 PersonAddress 等类相同)。

或者,使用您现有的代码并在顶部扔一个 list.clear()

,

一旦您了解了 TrialIterationInvocation 之间的区别,您的问题就变得很容易回答了。还有什么地方比 samples themselves 更好地理解这些。

Invocation 是方法的单次执行。假设有 3 个线程,每个线程执行此基准测试方法 100 次。这意味着 Invocation == 300。这就是为什么使用它作为设置会得到非常相似的结果。

Iteration 将是上面示例中的 3

Trial 将是 1,当所有线程执行其所有方法时。

Invocation,虽然有一个可怕的文档有它的用法,like a sorted data structure;但我也在其他各种地方使用过。此外,operation 的概念可以用 @OperationsPerInvocation “改变” - 这是另一个尖锐的工具。


有了这个 - 很容易回答。当您使用 Iteration 时,您的 ArrayList 会不断增长 - 这在内部意味着 System::arrayCopy,而您的数组不会。

弄清楚这一点后,您需要阅读示例并了解第二个问题是您的 @Benchmark 方法返回 void。而且,与另一个答案相反 - 我不建议使用测试方法本身来批量处理所有内容,但这提出了一个问题,即您要测试什么,首先。不要忘记这些只是数字,最后,您需要推理它们的含义以及如何正确设置 JMH 测试。

,

即使最初认为这是一种自然的性能差异,下面的评论也是正确的


正如下面评论的,差异确实高于预期。

add()O(1) 变为 O(n) 的唯一情况是它增长。可能是测试重用了相同的数组列表(因为 setup 没有被多次调用)?这只会影响 arraylist 测试,因为数组只会覆盖值。

只是为了确保数组列表没有增长:

public void capacityTestArrayListEnsured(Params p) 
{
    p.peopleArrayListEnsure = new ArrayList<>(p.length); //or clear()?
    for (int i = 0; i < p.length; i++) 
        p.peopleArrayListEnsure.add(new Person(i,i)));
}

为了公平起见,您还可以在其他方法中初始化数组,以便相等添加经过的时间:

public void capacityTestArray(Params p)  
{
    p.people = new Person[p.length];
    for (int i = 0; i < p.length; i++) 
        p.people[i] = new Person(i,i));
}