为什么迭代通过List <string>比split string慢并且迭代StringBuilder?

时间:2016-06-13 11:03:04

标签: java string list loops 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更慢。

任何人都可以向我解释这个吗?

3 个答案:

答案 0 :(得分:5)

(这是一个完全修改过的答案。请参阅 1 了解原因。感谢Buhb让我再看看!注意他/她也{ {3}}。)

请注意您的结果,Java中的微基准测试非常棘手,您的基准测试代码正在进行I / O等工作;请参阅此问题及其答案:posted an answer

事实上,据我所知,你的结果误导了你(和我,最初)。虽然for数组上的增强String循环ArrayList<String>上的.toString().split(" ")更快(下面有更多内容),ArrayList开销似乎仍占主导地位,并使该版本比$版本慢。显着慢了。

让我们使用经过精心设计和测试的微工程标记工具来确定哪个更快:How do I write a correct micro-benchmark in Java?

我正在使用Linux,所以我在这里设置它(test只是为了表示命令提示符;你在后输入的是):

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

这会在src/main/java/org/sample/MyBenchmark.java子目录中创建基准项目,所以:

$ cd test

3。在生成的项目中,我删除了默认的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"); } } }

-f 4

4。现在我们进行测试,我们构建项目:

$ mvn clean install

5。我们准备好测试了!关闭所有不需要运行的程序,然后关闭此命令。 这需要一段时间,,并且您希望在此过程中让您的机器独立。去抓一杯o'Java。

$ java -jar target/benchmarks.jar -f 4 -wi 10 -i 10

(注意:-wi 10表示“只做四个叉子,而不是十个”; -i 10表示“只做10次热身,而不是20次;”和List意思是“只进行10次测试迭代,而不是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次操作,而分裂并循环数组版本则少于5000次操作/秒。

因此,由于执行.toString().split(" ")的成本,List版本会更快,因此您最初的期望是正确的。这样做并循环结果比使用for明显要慢。

关于String[]List<String>的增强型String[]:循环List<String>比通过{{1} <循环显着更快因此,.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个迭代);结果如下:

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

比列表更接近一个数量级的操作/秒循环数组。

这并不让我感到惊讶,因为增强的for循环可以直接在数组上工作,但必须使用IteratorList返回的List case和make方法调用它:每个循环两次调用(Iterator#hasNextIterator#next),用于10,001次循环= 20,002次调用。方法调用很便宜,但它们不是免费的,即使JIT内联它们,这些调用的代码仍然必须运行。 ArrayList的{​​{1}}必须先做一些工作才能返回下一个数组条目,而当增强的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

  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

useArray

  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

因此,我们可以看到useList直接在数组上运行,我们可以看到useArrayuseList方法的两次调用。

当然,大多数时候没关系。除非你已经将优化的代码确定为瓶颈,否则不要担心这些事情。

1 这个答案已经从它的原始版本进行了彻底的修改,因为我在原始版本中假设了split-then-loop-array版本更快的断言是真的。我完全没有检查那个断言,只是跳到分析增强Iterator循环在数组上比列表更快的分析。我的错。非常感谢JMH让我仔细看看。

答案 1 :(得分:2)

如果split您正在直接操作数组,那么速度非常快。 ArrayList在内部使用数组,但在它周围添加了一些代码,因此它必须比在纯数组上迭代慢。

但是说我根本不会使用这样的微基准 - 在JIT运行之后,结果可能会有所不同。

更重要的是,做一些更具可读性的内容,在遇到问题时担心性能,而不是之前 - 更干净的代码在开始时更好。

答案 2 :(得分:2)

由于各种优化和JIT编译,对Java进行基准测试很难。

我很遗憾地说你的测试没有得出任何结论。您必须做的最少是创建两个不同的程序,每个方案一个,并单独运行它们。我扩展了你的代码,写了这个:

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,我得到的执行时间是毫秒,而不是几十毫秒,这告诉我分裂vs list looping不能负责一种方法占用另一种方法的两倍。

通常,IO相对较慢,因此您的代码大部分时间都在执行println语句。

编辑: 好的,所以我现在已经完成了我的作业。受到@StephenC提供的链接的启发,并使用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

所以对我来说,看起来列表版本更快,这与你最初的直觉一致。