【问题标题】:Why is iteration through List<String> slower than split string and iterate over StringBuilder?为什么通过 List<String> 迭代比拆分字符串和迭代 StringBuilder 慢?
【发布时间】:2016-10-13 18:35:33
【问题描述】:

我想知道为什么每个循环的 List&lt;String&gt;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 秒
  • List 的执行时间为 0,06000 秒

我希望 StringBuilder 会因为 b.toString().split(" ")) 而变慢。

谁能给我解释一下?

【问题讨论】:

  • 这对我来说很可疑。我预计 System.out.println 的 IO 会占用绝大多数时间,这将导致两种场景的时间非常相似。
  • 我不相信您从该基准测试中获得的结果。这是有缺陷的。问题 1) 没有 JVM 预热,2) 你没有衡量你认为自己是什么。阅读所有这些 .... daniel.mitterdorfer.name/categories/microbenchmark .... 你会开始明白我指的是什么。

标签: java string list loops stringbuilder


【解决方案1】:

(这是一个完全修改过的答案。请参阅 1 了解原因。感谢 Buhb 让我再看一遍!请注意,他/她也有 posted an answer .)


注意您的结果,Java 中的微基准测试非常棘手,而且您的基准测试代码正在执行 I/O 等操作;有关更多信息,请参阅此问题及其答案:How do I write a correct micro-benchmark in Java?

事实上,据我所知,您的结果误导了您(以及最初的我)。尽管String 数组上的增强型for 循环ArrayList&lt;String&gt; 上的循环要快得多(更多内容见下文),但.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&lt;String&gt; 上的增强型for:循环通过String[] 比通过List&lt;String&gt;明显,因此.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#hasNextIterator#next),10,001 次循环 = 20,002 次调用。方法调用很便宜,但它们不是免费的,即使 JIT 内联它们,这些调用的代码 仍然必须运行。 ArrayListListIterator 必须做一些工作才能返回下一个数组条目,而当增强的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直接对数组进行操作,我们可以看到useListIterator方法的两次调用。

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


1 这个答案已经从它的原始版本彻底修改,因为我在原始版本中假设分割然后循环数组版本更快的断言是正确的。我完全没有检查这个断言,只是开始分析增强的for 循环在数组上比列表上更快。我的错。再次感谢Buhb 让我仔细观察。

【讨论】:

    【解决方案2】:

    split 的情况下,您直接对数组进行操作,因此速度非常快。 ArrayList 在内部使用数组,但在其周围添加了一些代码,因此它必须比遍历纯数组要慢。

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

    更重要的是,做更易读的事情,当你遇到问题时担心性能,而不是之前 - 更干净的代码在开始时会更好。

    【讨论】:

    • 你有链接到我可以阅读材料的来源吗?
    【解决方案3】:

    对 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,我会得到毫秒的执行时间,而不是几十毫秒,这告诉我拆分 vs列表循环不能对一种方法花费另一种方法的时间负责。

    一般来说,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
    

    所以在我看来,列表版本似乎更快,这与您最初的直觉一致。

    【讨论】:

      猜你喜欢
      • 2019-07-30
      • 1970-01-01
      • 2012-05-16
      • 2011-06-02
      • 2018-05-12
      • 1970-01-01
      • 1970-01-01
      • 2015-10-20
      • 2014-07-14
      相关资源
      最近更新 更多