【问题标题】:Slow string concatenation over large input大输入上的慢字符串连接
【发布时间】:2010-11-10 17:09:47
【问题描述】:

我写了一个工作正常的 n 叉树 ADT。但是,我需要将其序列化存储在调用类的变量中。例如。

    DomTree<String> a = Data.createTreeInstance("very_large_file.xml");
    String x = a.toString();

我已经编写了完全符合我需要的方法,但是在非常大的输入上它需要很长时间(100MB xml 文件需要 20 分钟) - 我已经对这些方法进行了计时,并且从 xml 文件构建树很快, 但是如上图调用 toString() 很慢。

@Override
public String toString(){
    return printTree(this);
}

public String printTree(AbstractTree<E> tree){
    if (tree.isLeaf()){
        return tree.getNodeName();
    }else{
        String tStr = tree.getNodeName() + "(";

        int i = 0;
        Iterator<AbstractTree<E>> child = tree.getChildren().iterator();
        while (i < tree.getChildren().size() - 1){

            tStr += printTree(child.next()) + ", ";
            i++;
        }
        tStr += printTree(child.next()) + ")";

        return tStr;    
    }
}

我猜这与字符串的构建方式有关,而不是与树的遍历方式有关?有没有更好的方法来做到这一点?

更新:按照 Skaffman 的示例,以下代码针对非常大的输入给出 outOfMemoryError。

@Override
public String toString(){
    StringBuilder buffer = new StringBuilder();
    printTree(this, buffer);
    return buffer.toString();

}

public String printTree(AbstractTree<E> tree, StringBuilder buffer){
    if (tree.isLeaf()){
        return tree.getNodeName();
    }else{
        buffer.append(tree.getNodeName());
        buffer.append("(");

        int i = 0;
        Iterator<AbstractTree<E>> child = tree.getChildren().iterator();
        while (i < tree.getChildren().size() - 1){

            buffer.append(printTree(child.next(), buffer));
            buffer.append(", ");
            i++;
        }
        buffer.append(printTree(child.next(), buffer)); 
        buffer.append(")");

        return buffer.toString();   
    }
}

更新:现在可以完美运行,使用 Skaffmans 示例

【问题讨论】:

  • 别猜了。给自己找一个分析器并测量它。
  • 好的,您现在正在混合和匹配新旧方法。我已经更新了我的答案,以向您完整展示我的意思。

标签: java optimization string-concatenation


【解决方案1】:

查看 StringBuilder,不要使用简单的连接,将 StringBuilder 传递给你的整个过程(或使其成为全局)。

【讨论】:

    【解决方案2】:

    这样的字符串连接非常慢。使用 StringBuilder。

    @Override
    public String toString(){
            StringBuilder buffer = new StringBuilder();
            printTree(this, buffer);
            return buffer.toString();
    }
    
    public void printTree(AbstractTree<E> tree, StringBuilder buffer){
        if (tree.isLeaf()){
            buffer.append(tree.getNodeName());
        } else {
            buffer.append(tree.getNodeName());
            buffer.append("(");
    
            int i = 0;
            Iterator<AbstractTree<E>> child = tree.getChildren().iterator();
            while (i < tree.getChildren().size() - 1){
                printTree(child.next(), buffer);
                buffer.append(", ");
                i++;
            }
            printTree(child.next(), buffer); 
            buffer.append(")");
        }
    }
    

    【讨论】:

    • 我已经按照您的示例进行操作,但出现了 outOfMemoryError。我已将 VM args 设置为 -Xms2g -Xmx2g,但这无济于事...
    • 方法返回的String的目的是什么?
    • 字符串的目的是插入我正在测试的几个距离度量算法。
    • 我敢说,但这是有状态的迭代编程比函数式编程更优越的一个例子吗?
    【解决方案3】:

    不要在循环中使用字符串连接。它无法扩展。

    使用StringBuilder,这不会一直生成新对象,比如字符串连接..

    void print() {
    StringBuilder sb = new StringBuilder();
    sb.append("hello");
    sb.append(" World!");
    System.out.println(sb.toString());
    

    }

    【讨论】:

    • 这是我认为的完美答案。连接在循环之外很好——事实上 JVM 对其进行了很好的优化,以至于它可能比使用任何替代方法都快,但是在循环中,性能就会消失。如果你想看到一些有趣的优化,请查看 String 源代码。
    • @Bill K:性能在一个循环中非常糟糕,以至于在最坏的情况下总连接成本为 O(n^2),对吧?正如我在回答中所说的那样。你能看看我的更新吗?
    • 我很佩服你的简单回答:非常适合像我这样从谷歌来到这里的人。 :)
    【解决方案4】:

    您可能希望将String.intern() 视为减少内存使用的一种方法。这将使用字符串池中的实习字符串。如果您有许多重复的字符串,它可能会更快。有关实习字符串的更多信息here

    【讨论】:

    • 问题不是字符串比较而是字符串拼接; imho String.intern() 在这种情况下无效
    【解决方案5】:

    让我说字符串连接慢的原因是因为字符串是不可变的。这意味着每次编写“+=”时,都会创建一个新字符串。这意味着您构建字符串的方式在最坏的情况下是 O(n2)。这是因为如果您一次 +='ed 1 个字符,则构建新字符串的成本将是 2 + 3 + 4 + ... + n,即 O(n2)。

    按照其他人的建议使用 StringBuilder(在较慢但线程安全的 StringBuffer 上)。

    我想我应该补充一下,StringBuilder 会给你 O(n) 的摊销时间,因为它在幕后工作就像一个向量,因为它是可变的。所以在那里建立你的字符串,然后调用 toString()。

    StringBuilder builder = new StringBuilder();
    builder.append("blah"); // append more as needed.
    String text = builder.toString();
    

    我还想补充一点,这个问题在 Python 中是类似的。 python中的习惯用法是将所有字符串附加到一个列表中,然后加入列表。 "".join(the_list).

    更新: 正如比尔指出的那样,串联并不是万恶之源。一次性字符串连接很好,甚至可以优化! (它们也是最坏情况下的线性)。但是,当您在循环中进行连接时,如上所示,随着迭代次数的增加,性能将发生巨大变化。在这种情况下,我的上述分析是完美无​​缺的,因为我特别指出这是“最坏情况”,这意味着您没有假设任何优化。 (JVM 甚至无法优化循环中的连接,也无法优化循环之外的连接)。

    【讨论】:

    • 理论上是对的,实际上你应该看看String类,一些连接实际上并没有分配新的字符串。用于存储字符串的内部数组可以在两个不同长度的字符串之间共享——因此它可以扩展,并且可以在现有字符串后面复制一个新字符串,并且两个字符串可以具有不同长度的相同后备数组。问题是,这只工作一次——在设置“共享”标志之后,你不能再这样做了——所以在循环中你是完全正确的。
    • 那么为什么是-1?我还特别说这是最坏情况下的表现……这绝对是正确的。最坏的情况意味着优化对你不利。
    • 但不是,在循环中。也许我应该更新和澄清。
    • 也许字符串连接不是二次的?也许它是线性 O(n+m) 其中 n = |str1|和 m = |str2| ?
    • 请看我的澄清答案。对于一次性的东西来说,它当然是线性的。但在最坏的情况下,当您进行一系列连接时,它是二次方的。
    【解决方案6】:

    如果分析器确认您瓶颈是字符串连接,您有两种选择:

    • StringBuilder/StringBuffer(后者更适合线程)
    • Ropes for Java:

    绳索是弦乐的高性能替代品。在“Ropes: an Alternative to Strings”中详细描述的数据结构,对于常见的字符串修改(如 prepend、append、delete 和 insert)提供了比 String 和 StringBuffer 更好的性能。与字符串一样,绳索是不可变的,因此非常适合用于多线程编程。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2014-02-25
      • 1970-01-01
      • 2012-01-07
      • 1970-01-01
      • 1970-01-01
      • 2013-02-17
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多