(这是一个完全修改过的答案。请参阅 1 了解原因。感谢 Buhb 让我再看一遍!请注意,他/她也有 posted an answer .)
注意您的结果,Java 中的微基准测试非常棘手,而且您的基准测试代码正在执行 I/O 等操作;有关更多信息,请参阅此问题及其答案:How do I write a correct micro-benchmark in Java?
事实上,据我所知,您的结果误导了您(以及最初的我)。尽管String 数组上的增强型for 循环 比ArrayList<String> 上的循环要快得多(更多内容见下文),但.toString().split(" ") 开销似乎仍然占主导地位并使该版本比ArrayList 版本慢。明显变慢了。
让我们使用经过全面设计和测试的微基准测试工具来确定哪个更快:JMH。
我使用的是 Linux,所以我是这样设置的($ 只是表示命令提示符;您键入的是 在之后):
1。首先,我安装了 Maven,因为我通常不安装它:
$ sudo apt-get 安装 maven
2。然后我使用 Maven 创建了一个示例基准项目:
$ mvn原型:生成\
-DinteractiveMode=假\
-DarchetypeGroupId=org.openjdk.jmh \
-DarchetypeArtifactId=jmh-java-benchmark-archetype \
-DgroupId=org.sample\
-DartifactId=测试\
-版本=1.0
这会在 test 子目录中创建基准项目,因此:
$ cd 测试
3。在生成的项目中,我删除了默认的src/main/java/org/sample/MyBenchmark.java,并在该文件夹中创建了三个文件用于基准测试:
Common.java:真无聊:
package org.sample;
public class Common {
public static final int LENGTH = 10001;
}
最初我预计那里需要更多...
TestList.java:
package org.sample;
import java.util.List;
import java.util.ArrayList;
import java.text.NumberFormat;
import java.text.DecimalFormat;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Scope;
public class TestList {
// This state class lets us set up our list once and reuse it for tests in this test thread
@State(Scope.Thread)
public static class TestState {
public final List<String> list;
public TestState() {
// Your code for creating the list
NumberFormat formatter = new DecimalFormat("#0.00000");
List<String> a = new ArrayList<String>();
for (int i = 0; i < Common.LENGTH; ++i)
{
a.add("String:" + i);
}
this.list = a;
}
}
// This is the test method JHM will run for us
@Benchmark
public void test(TestState state) {
// Grab the list
final List<String> strings = state.list;
// Loop through it -- note that I'm doing work within the loop, but not I/O since
// we don't want to measure I/O, we want to measure loop performance
int l = 0;
for (String s : strings) {
l += s == null ? 0 : 1;
}
// I always do things like this to ensure that the test is doing what I expected
// it to do, and so that I actually use the result of the work from the loop
if (l != Common.LENGTH) {
throw new RuntimeException("Test error");
}
}
}
TestStringSplit.java:
package org.sample;
import java.text.NumberFormat;
import java.text.DecimalFormat;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Scope;
@State(Scope.Thread)
public class TestStringSplit {
// This state class lets us set up our list once and reuse it for tests in this test thread
@State(Scope.Thread)
public static class TestState {
public final StringBuffer sb;
public TestState() {
NumberFormat formatter = new DecimalFormat("#0.00000");
StringBuffer b = new StringBuffer();
for (int i = 0; i < Common.LENGTH; ++i)
{
b.append("String:" + i + " ");
}
this.sb = b;
}
}
// This is the test method JHM will run for us
@Benchmark
public void test(TestState state) {
// Grab the StringBuffer, convert to string, split it into an array
final String[] strings = state.sb.toString().split(" ");
// Loop through it -- note that I'm doing work within the loop, but not I/O since
// we don't want to measure I/O, we want to measure loop performance
int l = 0;
for (String s : strings) {
l += s == null ? 0 : 1;
}
// I always do things like this to ensure that the test is doing what I expected
// it to do, and so that I actually use the result of the work from the loop
if (l != Common.LENGTH) {
throw new RuntimeException("Test error");
}
}
}
4。现在我们有了测试,我们构建项目:
$ mvn 全新安装
5。我们已经准备好进行测试了!关闭您不需要运行的所有程序,然后启动此命令。 这需要一些时间,并且您希望在此过程中不理会您的机器。去喝杯 o'Java。
$ java -jar 目标/benchmarks.jar -f 4 -wi 10 -i 10
(注意:-f 4 的意思是“只做 4 次分叉,而不是 10 次”;-wi 10 的意思是“只做 10 次预热迭代,而不是 20 次;”而-i 10 的意思是“只做 10 次测试迭代,而不是 20 英寸。如果你想真正严谨,就把它们放下,去吃午饭,而不是只喝咖啡休息。)
这是我在 64 位 Intel 机器上使用 JDK 1.8.0_74 得到的结果:
基准模式 Cnt 得分误差单位
TestList.test thrpt 40 65641.040 ± 3811.665 ops/s
TestStringSplit.test thrpt 40 4909.565 ± 33.822 ops/s
loop-through-list 版本每秒执行超过 65k 次操作,而 split-and-loop-through-array 版本则少于 5000 ops/sec。
因此,由于执行.toString().split(" ") 的成本,您最初期望List 版本会更快是正确的。这样做并循环结果明显比使用 List 慢。
关于String[] 与List<String> 上的增强型for:循环通过String[] 比通过List<String> 快明显,因此.toString().split(" ") 必须有成本我们很多。为了只测试循环部分,我之前使用了带有 TestList 类的 JMH,以及这个 TestArray 类:
package org.sample;
import java.util.List;
import java.util.ArrayList;
import java.text.NumberFormat;
import java.text.DecimalFormat;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Scope;
public class TestArray {
// This state class lets us set up our list once and reuse it for tests in this test thread
@State(Scope.Thread)
public static class TestState {
public final String[] array;
public TestState() {
// Create an array with strings like the ones in the list
NumberFormat formatter = new DecimalFormat("#0.00000");
String[] a = new String[Common.LENGTH];
for (int i = 0; i < Common.LENGTH; ++i)
{
a[i] = "String:" + i;
}
this.array = a;
}
}
// This is the test method JHM will run for us
@Benchmark
public void test(TestState state) {
// Grab the list
final String[] strings = state.array;
// Loop through it -- note that I'm doing work within the loop, but not I/O since
// we don't want to measure I/O, we want to measure loop performance
int l = 0;
for (String s : strings) {
l += s == null ? 0 : 1;
}
// I always do things like this to ensure that the test is doing what I expected
// it to do, and so that I actually use the result of the work from the loop
if (l != Common.LENGTH) {
throw new RuntimeException("Test error");
}
}
}
我像之前的测试一样运行它(四次分叉、10 次热身和 10 次迭代);结果如下:
基准模式 Cnt 得分误差单位
TestArray.test thrpt 40 568328.087 ± 580.946 操作/秒
TestList.test thrpt 40 62069.305 ± 3793.680 操作/秒
遍历数组的操作/秒比列表多出近一个数量级。
这并不让我感到惊讶,因为增强的for 循环可以直接在数组上工作,但必须在List 情况下使用List 返回的Iterator 并对其进行方法调用:每个循环两次调用(Iterator#hasNext 和 Iterator#next),10,001 次循环 = 20,002 次调用。方法调用很便宜,但它们不是免费的,即使 JIT 内联它们,这些调用的代码 仍然必须运行。 ArrayList 的ListIterator 必须做一些工作才能返回下一个数组条目,而当增强的for 循环知道它正在处理一个数组时,它可以直接在它上面工作。
上面的测试类都有测试杂乱无章,但是要了解为什么数组版本更快,让我们看一下这个更简单的程序:
import java.util.List;
import java.util.ArrayList;
public class Example {
public static final void main(String[] args) throws Exception {
String[] array = new String[10];
List<String> list = new ArrayList<String>(array.length);
for (int n = 0; n < array.length; ++n) {
array[n] = "foo" + System.currentTimeMillis();
list.add(array[n]);
}
useArray(array);
useList(list);
System.out.println("Done");
}
public static void useArray(String[] array) {
System.out.println("Using array:");
for (String s : array) {
System.out.println(s);
}
}
public static void useList(List<String> list) {
System.out.println("Using list:");
for (String s : list) {
System.out.println(s);
}
}
}
使用javap -c Example编译后,我们可以查看useXYZ这两个函数的字节码;我已将每个循环部分加粗,并将它们与每个函数的其余部分稍微分开:
useArray:
公共静态无效 useArray(java.lang.String[]);
代码:
0: getstatic #15 // 字段 java/lang/System.out:Ljava/io/PrintStream;
3: ldc #18 // 字符串使用数组:
5: invokevirtual #17 // 方法 java/io/PrintStream.println:(Ljava/lang/String;)V
8:aload_0
9:astore_1
10:aload_1
11:数组长度
12:istore_2
13:iconst_0
14:istore_3
15:iload_3
16:iload_2
17: if_icmpge 39
20:aload_1
21:iload_3
22:加载
23:4 号店
25: getstatic #15 // 字段 java/lang/System.out:Ljava/io/PrintStream;
28:加载 4
30: invokevirtual #17 // 方法 java/io/PrintStream.println:(Ljava/lang/String;)V
33: iinc 3, 1
36:转到 15
39:返回
useList:
公共静态无效使用列表(java.util.List);
代码:
0: getstatic #15 // 字段 java/lang/System.out:Ljava/io/PrintStream;
3: ldc #19 // 字符串使用列表:
5: invokevirtual #17 // 方法 java/io/PrintStream.println:(Ljava/lang/String;)V
8:aload_0
9: invokeinterface #20, 1 // 接口方法 java/util/List.iterator:()Ljava/util/Iterator;
14:astore_1
15:加载_1
16: invokeinterface #21, 1 // 接口方法 java/util/Iterator.hasNext:()Z
21:如果当量 44
24:加载_1
25: invokeinterface #22, 1 // 接口方法 java/util/Iterator.next:()Ljava/lang/Object;
30: checkcast #2 // 类 java/lang/String
33:astore_2
34: getstatic #15 // 字段 java/lang/System.out:Ljava/io/PrintStream;
37:加载_2
38: invokevirtual #17 // 方法 java/io/PrintStream.println:(Ljava/lang/String;)V
41:转到 15
44:返回
所以我们可以看到useArray直接对数组进行操作,我们可以看到useList对Iterator方法的两次调用。
当然,大多数时候没关系。除非您确定要优化的代码是瓶颈,否则不要担心这些事情。
1 这个答案已经从它的原始版本彻底修改,因为我在原始版本中假设分割然后循环数组版本更快的断言是正确的。我完全没有检查这个断言,只是开始分析增强的for 循环在数组上比列表上更快。我的错。再次感谢Buhb 让我仔细观察。