如何正确使用JMH? ArrayList 示例
How to use JMH properly? Example with ArrayList
在我的示例中,理论上 2 种方法的性能应该非常相似。在第一种情况下我使用数组,在第二种情况下 - ArrayList 确保容量。
结果如下:
LessonBenchmark2.capacityTestArray avgt 5
1,354 ± 0,057 ms/op
LessonBenchmark2.capacityTestArrayListEnsured
avgt 5 32,018 ± 81,911 ms/op
这里似乎数组要快得多(1.354 对 32.018 ms/op)。可能是我对 JMH 的基准设置不正确。怎么弄好?
此外,如果我使用@Setup(Level.Invocation),那么结果很接近(1,405 对 1,496 ms/op):
LessonBenchmark.capacityTestArray avgt 5 1,405 ± 0,143
ms/op
LessonBenchmark.capacityTestArrayListEnsured avgt 5 1,496 ± 0,104
ms/op
不过据说要小心使用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, new Address(i, i), new Pet(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;
}
}
即使最初认为这是自然的性能差异,但下面的评论是正确的
如下评论,差异确实高于预期。
add()
从 O(1)
变为 O(n)
的唯一情况是 增长 。可能是测试重用了相同的数组列表(因为设置没有被多次调用)?这只会影响数组列表测试,因为数组只会覆盖值。
只是为了确保数组列表没有增长:
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, new Address(i, i), new Pet(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, new Address(i, i), new Pet(i, i));
}
测试设计不当;在你的测试中,因为 arraylist 只为多次调用创建一次,基于数组的代码只是多次覆盖同一个数组,而 arraylist 版本添加的越来越多,并且需要增长。
一个简单的解决方法是先清除它。另一个解决方法是在此处停止使用状态,只创建对象(无论是 100k 人数组,还是 person 数组列表,为 100k 人预先确定大小)作为测试工具的一部分。一旦你解决了这个问题,考虑到错误,结果是完全相同的,对于这个[=29],数组和数组列表之间的性能完全没有差异 =].
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, new Address(i, i), new Pet(i, i)));
}
}
(保持所有注释和 Person
、Address
等 类 相同)。
或者,使用您现有的代码并在顶部添加一个 list.clear()
。
一旦您理解了 Trial
、Iteration
和 Invocation
之间的区别,您的问题就会变得很容易回答。还有什么地方可以更好地理解这些 samples themselves.
Invocation
是该方法的单次执行。假设有 3 个线程,每个线程执行此基准测试方法 100 次。这意味着 Invocation == 300
。这就是为什么使用它作为设置会得到非常相似的结果。
Iteration
将是上述示例中的 3
。
当所有线程都执行所有方法时,Trial
将是 1
。
Invocation
,虽然有一个可怕的文档有它的用法,like a sorted data structure;但我也在其他各个地方使用过。 operation
的概念也可以用 @OperationsPerInvocation
“改变”——这是另一个利器。
有了这个 - 就很容易回答了。当您使用 Iteration
时,您的 ArrayList
将不断增长 - 这在内部意味着 System::arrayCopy
,而您的数组则不会。
弄清楚这一点后,您需要阅读示例并了解您的第二个问题是您的 @Benchmark
方法 return void
。而且,与另一个答案相反 - 我不建议使用测试方法本身来批量处理所有内容,但这提出了一个问题,即您首先要测试什么。不要忘记这些只是数字,最后,您需要推理它们的含义以及如何正确设置 JMH
测试。
在我的示例中,理论上 2 种方法的性能应该非常相似。在第一种情况下我使用数组,在第二种情况下 - ArrayList 确保容量。
结果如下:
LessonBenchmark2.capacityTestArray avgt 5 1,354 ± 0,057 ms/op
LessonBenchmark2.capacityTestArrayListEnsured avgt 5 32,018 ± 81,911 ms/op
这里似乎数组要快得多(1.354 对 32.018 ms/op)。可能是我对 JMH 的基准设置不正确。怎么弄好?
此外,如果我使用@Setup(Level.Invocation),那么结果很接近(1,405 对 1,496 ms/op):
LessonBenchmark.capacityTestArray avgt 5 1,405 ± 0,143 ms/op
LessonBenchmark.capacityTestArrayListEnsured avgt 5 1,496 ± 0,104 ms/op
不过据说要小心使用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, new Address(i, i), new Pet(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;
}
}
即使最初认为这是自然的性能差异,但下面的评论是正确的
如下评论,差异确实高于预期。
add()
从 O(1)
变为 O(n)
的唯一情况是 增长 。可能是测试重用了相同的数组列表(因为设置没有被多次调用)?这只会影响数组列表测试,因为数组只会覆盖值。
只是为了确保数组列表没有增长:
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, new Address(i, i), new Pet(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, new Address(i, i), new Pet(i, i));
}
测试设计不当;在你的测试中,因为 arraylist 只为多次调用创建一次,基于数组的代码只是多次覆盖同一个数组,而 arraylist 版本添加的越来越多,并且需要增长。
一个简单的解决方法是先清除它。另一个解决方法是在此处停止使用状态,只创建对象(无论是 100k 人数组,还是 person 数组列表,为 100k 人预先确定大小)作为测试工具的一部分。一旦你解决了这个问题,考虑到错误,结果是完全相同的,对于这个[=29],数组和数组列表之间的性能完全没有差异 =].
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, new Address(i, i), new Pet(i, i)));
}
}
(保持所有注释和 Person
、Address
等 类 相同)。
或者,使用您现有的代码并在顶部添加一个 list.clear()
。
一旦您理解了 Trial
、Iteration
和 Invocation
之间的区别,您的问题就会变得很容易回答。还有什么地方可以更好地理解这些 samples themselves.
Invocation
是该方法的单次执行。假设有 3 个线程,每个线程执行此基准测试方法 100 次。这意味着 Invocation == 300
。这就是为什么使用它作为设置会得到非常相似的结果。
Iteration
将是上述示例中的 3
。
Trial
将是 1
。
Invocation
,虽然有一个可怕的文档有它的用法,like a sorted data structure;但我也在其他各个地方使用过。 operation
的概念也可以用 @OperationsPerInvocation
“改变”——这是另一个利器。
有了这个 - 就很容易回答了。当您使用 Iteration
时,您的 ArrayList
将不断增长 - 这在内部意味着 System::arrayCopy
,而您的数组则不会。
弄清楚这一点后,您需要阅读示例并了解您的第二个问题是您的 @Benchmark
方法 return void
。而且,与另一个答案相反 - 我不建议使用测试方法本身来批量处理所有内容,但这提出了一个问题,即您首先要测试什么。不要忘记这些只是数字,最后,您需要推理它们的含义以及如何正确设置 JMH
测试。