为什么将第一项添加到集合中比第二项慢得多?
why adding first item into collection much slower than second?
我发现将第 1 项和第 2 项添加到集合中存在巨大的性能差异(尝试了 ArrayList
和 HashSet
),但我无法解释原因。已搜索但未找到任何答案。
public class Main {
public static void main(String[] args) {
// also tried HashSet
// also tried new ArrayList<>(2)
ArrayList<String> collection = new ArrayList<>();
long t1 = System.nanoTime();
collection.add("a");
long t2 = System.nanoTime();
collection.add("b");
long t3 = System.nanoTime();
System.out.println(String.valueOf(t2 - t1) + "\n"
+ String.valueOf(t3 - t2));
//typical output:
//4399
//1201
}
}
一些猜测:
- 因为在添加第一项时集合被延迟初始化?
- 或者我使用了错误的方式来衡量性能?
- 或与 jvm 的工作原理有关(这超出了我的知识范围)?
环境:jdk11、win10、intellij。
你的三个猜测都是正确的:)
你问得好。请注意:微基准测试 非常危险 以至于您可能很容易对代码各部分的速度做出完全错误的假设。您的惰性初始化是正确的,但运气不错 :)
最重要的是,您“使用错误的方式衡量绩效”的猜测是正确的。
您无法像这样测量 Java 应用程序的速度。这根本不可能。该过程太随机并且太依赖于许多其他因素,尤其是 运行 时间内的 JIT(即时优化)。
试试看这里:https://www.baeldung.com/java-microbenchmark-harness,尝试 运行 并思考它是如何工作的。
TL;DR:JVM 必须首先“预热”,然后您必须多次 运行 测试代码并计算平均时间。而且仍然可能有很多优化导致一些代码根本没有执行:)
如果你不想玩微基准库,至少将你的代码移到一个方法中并调用该方法 20 次。我刚刚做了(只是把\n
换成了---
),结果是这样的:
10800---1400
1500---200
600---100
500---100
700---100
400---100
400---100
400---100
400---100
500---100
400---100
400---100
400---100
400---100
300---100
400---100
300---100
500---100
400---100
300---100
如您所见,热身是最重要的因素。然而延迟初始化的影响也是可见的。
这是因为延迟初始化。当你运行这条线
ArrayList<String> collection = new ArrayList<>();
它只持有对列表的引用,但实际的内存分配不会发生在该列表上。但是,当您将第一个元素添加到集合中时,它首先会在添加第一个值之后为列表的下 10 个元素(10 是数组列表的默认大小)分配内存。
接下来的 9 个元素的结果将花费更少的时间来插入,但是对于第 11 个元素,它会比以前花费更多的时间。
public static void main(String[] args) {
ArrayList<String> collection = new ArrayList<>();
for (int i = 0; i < 12; i++) {
long t1 = System.nanoTime();
collection.add("a");
long t2 = System.nanoTime();
System.out.println("Index : "+ (i+1) +": Time: "+ String.valueOf(t2 - t1));
}
/** Output:
* Index : 1: Time: 6800
Index : 2: Time: 800
Index : 3: Time: 500
Index : 4: Time: 700
Index : 5: Time: 600
Index : 6: Time: 500
Index : 7: Time: 600
Index : 8: Time: 600
Index : 9: Time: 500
Index : 10: Time: 500
Index : 11: Time: 2800
Index : 12: Time: 500
*/
}
我发现将第 1 项和第 2 项添加到集合中存在巨大的性能差异(尝试了 ArrayList
和 HashSet
),但我无法解释原因。已搜索但未找到任何答案。
public class Main {
public static void main(String[] args) {
// also tried HashSet
// also tried new ArrayList<>(2)
ArrayList<String> collection = new ArrayList<>();
long t1 = System.nanoTime();
collection.add("a");
long t2 = System.nanoTime();
collection.add("b");
long t3 = System.nanoTime();
System.out.println(String.valueOf(t2 - t1) + "\n"
+ String.valueOf(t3 - t2));
//typical output:
//4399
//1201
}
}
一些猜测:
- 因为在添加第一项时集合被延迟初始化?
- 或者我使用了错误的方式来衡量性能?
- 或与 jvm 的工作原理有关(这超出了我的知识范围)?
环境:jdk11、win10、intellij。
你的三个猜测都是正确的:)
你问得好。请注意:微基准测试 非常危险 以至于您可能很容易对代码各部分的速度做出完全错误的假设。您的惰性初始化是正确的,但运气不错 :)
最重要的是,您“使用错误的方式衡量绩效”的猜测是正确的。
您无法像这样测量 Java 应用程序的速度。这根本不可能。该过程太随机并且太依赖于许多其他因素,尤其是 运行 时间内的 JIT(即时优化)。
试试看这里:https://www.baeldung.com/java-microbenchmark-harness,尝试 运行 并思考它是如何工作的。
TL;DR:JVM 必须首先“预热”,然后您必须多次 运行 测试代码并计算平均时间。而且仍然可能有很多优化导致一些代码根本没有执行:)
如果你不想玩微基准库,至少将你的代码移到一个方法中并调用该方法 20 次。我刚刚做了(只是把\n
换成了---
),结果是这样的:
10800---1400
1500---200
600---100
500---100
700---100
400---100
400---100
400---100
400---100
500---100
400---100
400---100
400---100
400---100
300---100
400---100
300---100
500---100
400---100
300---100
如您所见,热身是最重要的因素。然而延迟初始化的影响也是可见的。
这是因为延迟初始化。当你运行这条线
ArrayList<String> collection = new ArrayList<>();
它只持有对列表的引用,但实际的内存分配不会发生在该列表上。但是,当您将第一个元素添加到集合中时,它首先会在添加第一个值之后为列表的下 10 个元素(10 是数组列表的默认大小)分配内存。 接下来的 9 个元素的结果将花费更少的时间来插入,但是对于第 11 个元素,它会比以前花费更多的时间。
public static void main(String[] args) {
ArrayList<String> collection = new ArrayList<>();
for (int i = 0; i < 12; i++) {
long t1 = System.nanoTime();
collection.add("a");
long t2 = System.nanoTime();
System.out.println("Index : "+ (i+1) +": Time: "+ String.valueOf(t2 - t1));
}
/** Output:
* Index : 1: Time: 6800
Index : 2: Time: 800
Index : 3: Time: 500
Index : 4: Time: 700
Index : 5: Time: 600
Index : 6: Time: 500
Index : 7: Time: 600
Index : 8: Time: 600
Index : 9: Time: 500
Index : 10: Time: 500
Index : 11: Time: 2800
Index : 12: Time: 500
*/
}