为什么遍历 List<String> 比拆分字符串并遍历 StringBuilder 慢?
Why is iteration through List<String> slower than split string and iterate over StringBuilder?
我想知道为什么每个循环的 List<String>
比 StringBuilder
上的每个循环慢
这是我的代码:
package nl.testing.startingpoint;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String args[]) {
NumberFormat formatter = new DecimalFormat("#0.00000");
List<String> a = new ArrayList<String>();
StringBuffer b = new StringBuffer();
for (int i = 0;i <= 10000; i++)
{
a.add("String:" + i);
b.append("String:" + i + " ");
}
long startTime = System.currentTimeMillis();
for (String aInA : a)
{
System.out.println(aInA);
}
long endTime = System.currentTimeMillis();
long startTimeB = System.currentTimeMillis();
for (String part : b.toString().split(" ")) {
System.out.println(part);
}
long endTimeB = System.currentTimeMillis();
System.out.println("Execution time from StringBuilder is " + formatter.format((endTimeB - startTimeB) / 1000d) + " seconds");
System.out.println("Execution time List is " + formatter.format((endTime - startTime) / 1000d) + " seconds");
}
}
结果是:
- StringBuilder 的执行时间为 0,03300 秒
- 执行时间列表为 0,06000 秒
由于 b.toString().split(" "))
.
,我预计 StringBuilder 会变慢
谁能给我解释一下?
(这是一个完全修订的答案。请参阅 1 了解原因。感谢 Buhb for making me take a second look! Note that he/she has also 。)
注意你的结果,Java 中的微基准测试非常棘手,你的基准测试代码正在做 I/O,等等;请参阅此问题及其答案以获取更多信息:How do I write a correct micro-benchmark in Java?
事实上,据我所知,你的结果误导了你(最初也是误导了我)。尽管 String
数组 上的增强 for
循环 比 ArrayList<String>
上的循环快得多(下面有更多内容),.toString().split(" ")
开销似乎仍然占主导地位,并使该版本比 ArrayList
版本慢。明显变慢。
让我们使用经过全面设计和测试的微基准测试工具来确定哪个更快:JMH。
我正在使用 Linux,所以我是这样设置的($
只是表示命令提示符;您输入的是 after那个):
1。首先,我安装了 Maven,因为我通常不会安装它:
$ sudo apt-get install maven
2。然后我使用 Maven 创建了一个示例基准项目:
$ mvn archetype:generate \
-DinteractiveMode=false \
-DarchetypeGroupId=org.openjdk.jmh \
-DarchetypeArtifactId=jmh-java-benchmark-archetype \
-DgroupId=org.sample \
-DartifactId=test \
-Dversion=1.0
这会在 test
子目录中创建基准项目,因此:
$ cd test
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 clean install
5。我们准备好进行测试了!关闭任何不需要 运行ning 的程序,然后关闭此命令。 这需要一段时间,并且您希望在此过程中不要管您的机器。去喝一杯o'Java.
$ java -jar target/benchmarks.jar -f 4 -wi 10 -i 10
(注:-f 4
表示"only do four forks, not ten";-wi 10
表示"only do 10 warmup-iterations, not 20;",-i 10
表示"only do 10 test iterations, not 20"。如果你想变得非常严谨,就不要再去吃午饭了,而不仅仅是喝杯咖啡休息一下。)
这是我在 64 位 Intel 机器上使用 JDK 1.8.0_74 得到的结果:
Benchmark Mode Cnt Score Error Units
TestList.test thrpt 40 65641.040 ± 3811.665 ops/s
TestStringSplit.test thrpt 40 4909.565 ± 33.822 ops/s
循环列表版本执行了超过 65k operations/second,而拆分和循环数组版本执行了不到 5000 ops/sec。
因此,由于执行 .toString().split(" ")
的成本,您最初期望 List
版本会更快是正确的。这样做并循环结果明显比使用 List
.
慢
关于 String[]
与 List<String>
上的增强 for
:显着 比 String[]
更快地循环通过 List<String>
,所以 .toString().split(" ")
一定让我们付出了很多。为了仅测试循环部分,我之前将 JMH 与 TestList
class 一起使用,而这个 TestArray
class:
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次迭代);这是结果:
Benchmark Mode Cnt Score Error Units
TestArray.test thrpt 40 568328.087 ± 580.946 ops/s
TestList.test thrpt 40 62069.305 ± 3793.680 ops/s
遍历数组比列表多 operations/second 近一个数量级。
这并不让我吃惊,因为增强的 for
循环可以直接在数组上工作,但必须使用由 [=29= 编辑的 Iterator
return ] 在 List
情况下并对其进行方法调用:每个循环两次调用(Iterator#hasNext
和 Iterator#next
)10,001 次循环 = 20,002 次调用。方法调用很便宜,但它们不是免费的,即使 JIT 内联它们,这些调用的 code 仍然必须 运行。 ArrayList
的 ListIterator
在它可以 return 下一个数组条目之前必须做一些工作,而当增强的 for
循环知道它正在处理一个数组时,它可以工作直接上去。
上面的测试 classes 中有测试问题,但要了解为什么数组版本更快,让我们看看这个更简单的程序:
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
:
public static void useArray(java.lang.String[]);
Code:
0: getstatic #15 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #18 // String Using array:
5: invokevirtual #17 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: aload_0
9: astore_1
10: aload_1
11: arraylength
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: aaload
23: astore 4
25: getstatic #15 // Field java/lang/System.out:Ljava/io/PrintStream;
28: aload 4
30: invokevirtual #17 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
33: iinc 3, 1
36: goto 15
39: return
useList
:
public static void useList(java.util.List);
Code:
0: getstatic #15 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #19 // String Using list:
5: invokevirtual #17 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: aload_0
9: invokeinterface #20, 1 // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
14: astore_1
15: aload_1
16: invokeinterface #21, 1 // InterfaceMethod java/util/Iterator.hasNext:()Z
21: ifeq 44
24: aload_1
25: invokeinterface #22, 1 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
30: checkcast #2 // class java/lang/String
33: astore_2
34: getstatic #15 // Field java/lang/System.out:Ljava/io/PrintStream;
37: aload_2
38: invokevirtual #17 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
41: goto 15
44: return
所以我们可以看到useArray
直接对数组进行操作,我们可以看到useList
对Iterator
方法的两次调用。
当然,大多数时候没关系。除非您确定要优化的代码是瓶颈,否则不要担心这些事情。
1 这个答案已经从它的原始版本彻底修改了,因为我在原始版本中假设了 split-then-loop-array 版本更快的断言是真的。我完全没有检查那个断言,只是跳入分析增强的 for
循环如何在数组上比在列表上更快。我的错。再次感谢 Buhb 让我仔细看看。
在split
的情况下,你直接在数组上操作,所以速度非常快。 ArrayList
在内部使用数组,但在其周围添加了一些代码,因此它必须比迭代纯数组慢。
但是说我根本不会使用这样的微基准测试 - 在 JIT 具有 运行 之后结果可能会有所不同。
更重要的是,做更具可读性的事情,在遇到问题时担心性能,而不是之前 - 更清晰的代码在开始时更好。
基准测试 java 很难,因为有各种优化和 JIT 编译。
很遗憾,您无法从测试中得出任何结论。您至少必须做的是创建两个不同的程序,每个方案一个,并且 运行 它们分开。我扩展了你的代码,并写下了这个:
NumberFormat formatter = new DecimalFormat("#0.00000");
List<String> a = new ArrayList<String>();
StringBuffer b = new StringBuffer();
for (int i = 0;i <= 10000; i++)
{
a.add("String:" + i);
b.append("String:" + i + " ");
}
long startTime = System.currentTimeMillis();
for (String aInA : a)
{
System.out.println(aInA);
}
long endTime = System.currentTimeMillis();
long startTimeB = System.currentTimeMillis();
for (String part : b.toString().split(" ")) {
System.out.println(part);
}
long endTimeB = System.currentTimeMillis();
long startTimeC = System.currentTimeMillis();
for (String aInA : a)
{
System.out.println(aInA);
}
long endTimeC = System.currentTimeMillis();
System.out.println("Execution time List is " + formatter.format((endTime - startTime) / 1000d) + " seconds");
System.out.println("Execution time from StringBuilder is " + formatter.format((endTimeB - startTimeB) / 1000d) + " seconds");
System.out.println("Execution time List second time is " + formatter.format((endTimeC - startTimeC) / 1000d) + " seconds");
它给了我以下结果:
Execution time List is 0.04300 seconds
Execution time from StringBuilder is 0.03200 seconds
Execution time List second time is 0.01900 seconds
此外,如果我删除循环中的 System.out.println 语句,而只是将字符串附加到 StringBuilder,我得到的执行时间以毫秒为单位,而不是几十毫秒,这告诉我拆分与列表循环不能对一种方法花费另一种方法两倍的时间负责。
一般来说,IO 比较慢,因此您的代码大部分时间都在执行 println 语句。
编辑:
好的,所以我现在已经完成了作业。受到@StephenC 提供的 link 的启发,并使用 JMH 创建了一个基准。
进行基准测试的方法如下:
public void loop() {
for (String part : b.toString().split(" ")) {
bh.consume(part);
}
}
public void loop() {
for (String aInA : a)
{
bh.consume(aInA);
}
结果:
Benchmark Mode Cnt Score Error Units
BenchmarkLoop.listLoopBenchmark avgt 200 55,992 ± 0,436 us/op
BenchmarkLoop.stringLoopBenchmark avgt 200 290,515 ± 0,975 us/op
所以对我来说,列表版本看起来更快,这与您最初的直觉一致。
我想知道为什么每个循环的 List<String>
比 StringBuilder
这是我的代码:
package nl.testing.startingpoint;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String args[]) {
NumberFormat formatter = new DecimalFormat("#0.00000");
List<String> a = new ArrayList<String>();
StringBuffer b = new StringBuffer();
for (int i = 0;i <= 10000; i++)
{
a.add("String:" + i);
b.append("String:" + i + " ");
}
long startTime = System.currentTimeMillis();
for (String aInA : a)
{
System.out.println(aInA);
}
long endTime = System.currentTimeMillis();
long startTimeB = System.currentTimeMillis();
for (String part : b.toString().split(" ")) {
System.out.println(part);
}
long endTimeB = System.currentTimeMillis();
System.out.println("Execution time from StringBuilder is " + formatter.format((endTimeB - startTimeB) / 1000d) + " seconds");
System.out.println("Execution time List is " + formatter.format((endTime - startTime) / 1000d) + " seconds");
}
}
结果是:
- StringBuilder 的执行时间为 0,03300 秒
- 执行时间列表为 0,06000 秒
由于 b.toString().split(" "))
.
谁能给我解释一下?
(这是一个完全修订的答案。请参阅 1 了解原因。感谢 Buhb for making me take a second look! Note that he/she has also
注意你的结果,Java 中的微基准测试非常棘手,你的基准测试代码正在做 I/O,等等;请参阅此问题及其答案以获取更多信息:How do I write a correct micro-benchmark in Java?
事实上,据我所知,你的结果误导了你(最初也是误导了我)。尽管 String
数组 上的增强 for
循环 比 ArrayList<String>
上的循环快得多(下面有更多内容),.toString().split(" ")
开销似乎仍然占主导地位,并使该版本比 ArrayList
版本慢。明显变慢。
让我们使用经过全面设计和测试的微基准测试工具来确定哪个更快:JMH。
我正在使用 Linux,所以我是这样设置的($
只是表示命令提示符;您输入的是 after那个):
1。首先,我安装了 Maven,因为我通常不会安装它:
$ sudo apt-get install maven
2。然后我使用 Maven 创建了一个示例基准项目:
$ mvn archetype:generate \ -DinteractiveMode=false \ -DarchetypeGroupId=org.openjdk.jmh \ -DarchetypeArtifactId=jmh-java-benchmark-archetype \ -DgroupId=org.sample \ -DartifactId=test \ -Dversion=1.0
这会在 test
子目录中创建基准项目,因此:
$ cd test
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 clean install
5。我们准备好进行测试了!关闭任何不需要 运行ning 的程序,然后关闭此命令。 这需要一段时间,并且您希望在此过程中不要管您的机器。去喝一杯o'Java.
$ java -jar target/benchmarks.jar -f 4 -wi 10 -i 10
(注:-f 4
表示"only do four forks, not ten";-wi 10
表示"only do 10 warmup-iterations, not 20;",-i 10
表示"only do 10 test iterations, not 20"。如果你想变得非常严谨,就不要再去吃午饭了,而不仅仅是喝杯咖啡休息一下。)
这是我在 64 位 Intel 机器上使用 JDK 1.8.0_74 得到的结果:
Benchmark Mode Cnt Score Error Units TestList.test thrpt 40 65641.040 ± 3811.665 ops/s TestStringSplit.test thrpt 40 4909.565 ± 33.822 ops/s
循环列表版本执行了超过 65k operations/second,而拆分和循环数组版本执行了不到 5000 ops/sec。
因此,由于执行 .toString().split(" ")
的成本,您最初期望 List
版本会更快是正确的。这样做并循环结果明显比使用 List
.
关于 String[]
与 List<String>
上的增强 for
:显着 比 String[]
更快地循环通过 List<String>
,所以 .toString().split(" ")
一定让我们付出了很多。为了仅测试循环部分,我之前将 JMH 与 TestList
class 一起使用,而这个 TestArray
class:
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次迭代);这是结果:
Benchmark Mode Cnt Score Error Units TestArray.test thrpt 40 568328.087 ± 580.946 ops/s TestList.test thrpt 40 62069.305 ± 3793.680 ops/s
遍历数组比列表多 operations/second 近一个数量级。
这并不让我吃惊,因为增强的 for
循环可以直接在数组上工作,但必须使用由 [=29= 编辑的 Iterator
return ] 在 List
情况下并对其进行方法调用:每个循环两次调用(Iterator#hasNext
和 Iterator#next
)10,001 次循环 = 20,002 次调用。方法调用很便宜,但它们不是免费的,即使 JIT 内联它们,这些调用的 code 仍然必须 运行。 ArrayList
的 ListIterator
在它可以 return 下一个数组条目之前必须做一些工作,而当增强的 for
循环知道它正在处理一个数组时,它可以工作直接上去。
上面的测试 classes 中有测试问题,但要了解为什么数组版本更快,让我们看看这个更简单的程序:
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
:
public static void useArray(java.lang.String[]); Code: 0: getstatic #15 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #18 // String Using array: 5: invokevirtual #17 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: aload_0 9: astore_1 10: aload_1 11: arraylength 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: aaload 23: astore 4 25: getstatic #15 // Field java/lang/System.out:Ljava/io/PrintStream; 28: aload 4 30: invokevirtual #17 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 33: iinc 3, 1 36: goto 15 39: return
useList
:
public static void useList(java.util.List); Code: 0: getstatic #15 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #19 // String Using list: 5: invokevirtual #17 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: aload_0 9: invokeinterface #20, 1 // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator; 14: astore_1 15: aload_1 16: invokeinterface #21, 1 // InterfaceMethod java/util/Iterator.hasNext:()Z 21: ifeq 44 24: aload_1 25: invokeinterface #22, 1 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object; 30: checkcast #2 // class java/lang/String 33: astore_2 34: getstatic #15 // Field java/lang/System.out:Ljava/io/PrintStream; 37: aload_2 38: invokevirtual #17 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 41: goto 15 44: return
所以我们可以看到useArray
直接对数组进行操作,我们可以看到useList
对Iterator
方法的两次调用。
当然,大多数时候没关系。除非您确定要优化的代码是瓶颈,否则不要担心这些事情。
1 这个答案已经从它的原始版本彻底修改了,因为我在原始版本中假设了 split-then-loop-array 版本更快的断言是真的。我完全没有检查那个断言,只是跳入分析增强的 for
循环如何在数组上比在列表上更快。我的错。再次感谢 Buhb 让我仔细看看。
在split
的情况下,你直接在数组上操作,所以速度非常快。 ArrayList
在内部使用数组,但在其周围添加了一些代码,因此它必须比迭代纯数组慢。
但是说我根本不会使用这样的微基准测试 - 在 JIT 具有 运行 之后结果可能会有所不同。
更重要的是,做更具可读性的事情,在遇到问题时担心性能,而不是之前 - 更清晰的代码在开始时更好。
基准测试 java 很难,因为有各种优化和 JIT 编译。
很遗憾,您无法从测试中得出任何结论。您至少必须做的是创建两个不同的程序,每个方案一个,并且 运行 它们分开。我扩展了你的代码,并写下了这个:
NumberFormat formatter = new DecimalFormat("#0.00000");
List<String> a = new ArrayList<String>();
StringBuffer b = new StringBuffer();
for (int i = 0;i <= 10000; i++)
{
a.add("String:" + i);
b.append("String:" + i + " ");
}
long startTime = System.currentTimeMillis();
for (String aInA : a)
{
System.out.println(aInA);
}
long endTime = System.currentTimeMillis();
long startTimeB = System.currentTimeMillis();
for (String part : b.toString().split(" ")) {
System.out.println(part);
}
long endTimeB = System.currentTimeMillis();
long startTimeC = System.currentTimeMillis();
for (String aInA : a)
{
System.out.println(aInA);
}
long endTimeC = System.currentTimeMillis();
System.out.println("Execution time List is " + formatter.format((endTime - startTime) / 1000d) + " seconds");
System.out.println("Execution time from StringBuilder is " + formatter.format((endTimeB - startTimeB) / 1000d) + " seconds");
System.out.println("Execution time List second time is " + formatter.format((endTimeC - startTimeC) / 1000d) + " seconds");
它给了我以下结果:
Execution time List is 0.04300 seconds
Execution time from StringBuilder is 0.03200 seconds
Execution time List second time is 0.01900 seconds
此外,如果我删除循环中的 System.out.println 语句,而只是将字符串附加到 StringBuilder,我得到的执行时间以毫秒为单位,而不是几十毫秒,这告诉我拆分与列表循环不能对一种方法花费另一种方法两倍的时间负责。
一般来说,IO 比较慢,因此您的代码大部分时间都在执行 println 语句。
编辑: 好的,所以我现在已经完成了作业。受到@StephenC 提供的 link 的启发,并使用 JMH 创建了一个基准。 进行基准测试的方法如下:
public void loop() {
for (String part : b.toString().split(" ")) {
bh.consume(part);
}
}
public void loop() {
for (String aInA : a)
{
bh.consume(aInA);
}
结果:
Benchmark Mode Cnt Score Error Units
BenchmarkLoop.listLoopBenchmark avgt 200 55,992 ± 0,436 us/op
BenchmarkLoop.stringLoopBenchmark avgt 200 290,515 ± 0,975 us/op
所以对我来说,列表版本看起来更快,这与您最初的直觉一致。