字符串连接真的那么慢吗?

Posted

技术标签:

【中文标题】字符串连接真的那么慢吗?【英文标题】:Is string concatenaion really that slow? 【发布时间】:2013-02-17 03:35:42 【问题描述】:

我目前正在研究字符串连接选项以及它们对整体性能的影响。我的测试用例产生的结果让我大吃一惊,我不确定我是否忽略了某些东西。

这里是交易:在 java 中执行 "something"+"somethingElse" 将(在编译时)在每次完成时创建一个新的 StringBuilder

对于我的测试用例,我正在从我的 HDD 加载一个文件,该文件包含 1661 行示例数据(经典的“Lorem Ipsum”)。这个问题不是关于 I/O 性能,而是关于不同字符串 concat 方法的性能。

public class InefficientStringConcat 

    public static void main(String[] agrs) throws Exception
        // Get a file with example data:

        System.out.println("Starting benchmark");
        // Read an measure:
        for (int i = 0; i < 10; i++)
            BufferedReader in = new BufferedReader(
                    new InputStreamReader(new FileInputStream(new File("data.txt")))
            );

            long start = System.currentTimeMillis();
            // Un-comment method to test:
            //inefficientRead(in);
            //betterRead(in);
            long end = System.currentTimeMillis();
            System.out.println("Took "+(end-start)+"ms");

            in.close();
        



    

    public static String betterRead(BufferedReader in) throws IOException
        StringBuilder b = new StringBuilder();
        String line;
        while ((line = in.readLine()) != null)
            b.append(line);
        
        return b.toString();
    

    public static String inefficientRead(BufferedReader in) throws IOException 
        String everything = "", line;
        while ((line = in.readLine()) != null)
            everything += line;
        
        return everything;
    

如您所见,两个测试的设置相同。结果如下:

使用inefficientRead()-方法

Starting benchmark
#1 Took 658ms
#2 Took 590ms
#3 Took 569ms
#4 Took 567ms
#5 Took 562ms
#6 Took 570ms
#7 Took 563ms
#8 Took 568ms
#9 Took 560ms
#10 Took 568ms

使用betterRead()-方法

Starting benchmark
#1 Took 42ms
#2 Took 10ms
#3 Took 5ms
#4 Took 7ms
#5 Took 16ms
#6 Took 3ms
#7 Took 4ms
#8 Took 5ms
#9 Took 5ms
#10 Took 13ms

我正在运行java-command 没有额外参数的测试。我正在运行MacMini3,1 from early 2009 和 Sun JDK 7:

[luke@BlackBox ~]$ java -version
java version "1.7.0_09"
Java(TM) SE Runtime Environment (build 1.7.0_09-b05)
Java HotSpot(TM) Client VM (build 23.5-b02, mixed mode)

这让我觉得差别很大。我在衡量这个时做错了什么,还是应该发生这种情况?

【问题讨论】:

嗯,10 次迭代不足以作为基准测试,但除此之外它似乎 很好。尝试将迭代次数增加到 100,000 次,并添加一个“热身”阶段,在该阶段调用该方法 10,000 次(不计时),让 JIT 进行一些内联​​。 @CameronSkinner 显然您没有考虑到每个高级迭代已经包含 1661 次方法调用。 @MarkoTopolnik:基准测试正在通过betterReadinefficientRead 方法进行测量,因此您应该多次调用这些方法。所以你是对的:你没有考虑被测方法的实现细节。 @CameronSkinner 至少调用这些方法 10,000 次只是为了热身的建议显然是错误的:JVM JIT 根据通过任何单个的次数代码行,这里发生在第十次外部迭代之前。从那开始,您要求执行 100,000 次完整的文件加载过程是非常不合理的。即使是最严格的统计学家,最大 100 的样本量也能满足。 @MarkoTopolnik:我们可以同意不同意。但我建议您详细了解 JIT 在内联整个方法调用等方面可以做什么。 【参考方案1】:

我在衡量这个时做错了什么,还是应该发生这种情况?

这是应该发生的。使用重复的字符串连接构造一个长字符串是一种已知的性能反模式:每个 连接必须创建一个新字符串,其中包含原始字符串的 副本 和一个 copy 附加字符串。你最终会得到 O(N2) 的性能。当您使用StringBuilder 时,大多数时候您只是 将附加字符串复制到缓冲区中。有时缓冲区需要用完空间并需要扩展(通过将现有数据复制到新缓冲区中),但这种情况并不经常发生(由于缓冲区扩展策略)。

请参阅我的article on string concatenation 了解详细信息 - 这是一篇非常古老的文章,所以早于StringBuilder,但基本面没有改变。 (基本上StringBuilder 类似于StringBuffer,但没有同步。)

【讨论】:

很好,现在我可以在哪里领取我的“得到 Jon Skeet 的回答”T 恤? 乔恩说的很棒。回溯一步,如果不熟悉,String 是finalimmutable【参考方案2】:

这正是应该发生的事情。 betterRead 需要线性时间; inefficientRead 需要二次方时间。

【讨论】:

@MarkoTopolnik:您误解了大 O 复杂度的含义。它衡量的是成本的增长率,而不是绝对成本。如果您将Thread.sleep(1000) 添加到betterRead 方法中,它仍然是O(n),但在挂钟时间上比O(n^2) 方法花费的时间要长得多,至少在n 变大之前是这样。 @MarkoTopolnik:我当然会暗示常数因素无关紧要。 从第一次关心渐近线,到后来优化常数因子。 没有不同的算法最终得到完全相同的常数因子,我很乐意在这里省略它们为何不同的讨论。

以上是关于字符串连接真的那么慢吗?的主要内容,如果未能解决你的问题,请参考以下文章

ifelse 真的每次都计算它的两个向量吗?慢吗?

你认为的.NET数据库连接池,真的是全部吗?

合并与空字符串连接

与 Pynsq 相比,Nsqjs 真的慢吗?

PyMySQL 是不是得到很好的支持?它比 MySQLdb 慢吗?

Rails 3 Cli 执行命令真的很慢吗?