【问题标题】:Performance and simplicity tradeoffs between String, StringBuffer, and StringBuilderString、StringBuffer 和 StringBuilder 之间的性能和简单性权衡
【发布时间】:2011-07-23 12:40:04
【问题描述】:

您有没有想过这种 Java 编程语言变化的影响?

String 类被认为是一个不可变的类(这个决定是经过深思熟虑的)。但是字符串连接真的很慢,我自己做了基准测试。于是 StringBuffer 诞生了。非常棒的课程,同步且速度非常快。但是有些人对一些同步块的性能成本不满意,于是引入了 StringBuilder。

但是,当使用 String 连接不多的对象时,类的不变性使其成为实现线程安全的一种非常自然的方式。当我们要管理多个字符串时,我可以理解 StringBuffer 的使用。但是,这是我的第一个问题:

  1. 例如,如果您有 10 个或更少的字符串要附加,您是否愿意用简单性来换取几毫秒的执行时间?

    我也对 StringBuilder 进行了基准测试。它比 StringBuffer 更有效(仅提高了 10%)。但是,如果在您的单线程程序中使用 StringBuilder,如果您有时想要更改设计以使用多个线程会发生什么?您必须更改 StringBuilder 的每个实例,如果您忘记了一个实例,则会产生一些奇怪的效果(考虑到可能出现的竞争条件)。

  2. 在这种情况下,您会以性能换取数小时的调试时间吗?

好的,就是这样。除了简单的问题(StringBuffer 比“+”更高效且线程安全,StringBuilder 比 StringBuffer 更快但没有线程安全)我想知道何时使用它们。

(重要:我知道它们之间的区别;这是一个与平台架构和一些设计决策有关的问题。)

【问题讨论】:

  • 我仍然不完全确定您的问题是什么,尽管有粗体部分。事实上,你的文字似乎包含了标题中提出的问题的答案,而粗体部分直接相互矛盾(即粗体问题与开头的“重要”通知相矛盾)并且似乎已经回答了Stack Overflow 上无数次。
  • 康拉德,我想知道有经验的编码员的意见。我今天想到了这个想法,想知道它是否有任何意义。也许我没有正确地问它(我的英语不是很好)。对此感到抱歉。你的权利,顶部的通知不好,我必须改变它。我想获得“何时使用它们”的答案,但不想选择效率(就像我在这里看到的其他问题一样)。
  • 我也对 StringBuilder 进行了基准测试。它比 StringBuffer 更有效(平均只有 10%) 显示基准,你确定你写得对吗,到目前为止我认为我还没有在这个网站上看到一个合适的微基准。
  • 真的很简单。尝试一次添加 100,000 个 String 对象(例如,使用 for 循环),您会发现需要更多时间。我已经这样做了 100 次,并做了一个简单的平均。
  • @santiagobasulto,是的,几乎可以肯定是一个糟糕的微基准测试。

标签: java architecture string


【解决方案1】:

只是对您的“StringBuilders and threads”评论的评论:即使在多线程程序中,非常很少想要跨多个线程构建字符串。通常,每个线程都会有一些数据集并从中创建一个字符串,通常是通过将多个字符串连接在一起。然后他们会将StringBuilder 转换为字符串,并且可以在线程之间安全地共享该字符串。

我认为我曾经见过由于线程之间共享 StringBuilder 而导致的错误。

我个人希望 StringBuffer 不存在 - 它处于 Java 的“让我们同步一切”阶段,导致 VectorHashtable 几乎被不同步的 ArrayList 和 @987654327 淘汰@ 来自 Java 2 的类。只花了一点时间,StringBuffer 的非同步等效项才到达。

所以基本上:

  • 当您不想执行操作并希望确保没有其他操作时使用字符串
  • 使用StringBuilder 执行操作,通常在短时间内进行
  • 避免使用StringBuffer,除非你真的非常需要它 - 正如我所说,我不记得曾经见过我会使用StringBuffer而不是StringBuilder的情况,当两者都可用时。

【讨论】:

  • 伟大的乔恩!谢谢!那是我一直在寻找的。我一直在编写一个有这个小问题的应用程序(几个线程共享一个简单的字符串),但这是一个奇怪且不常见的例子。与收藏品的巨大对比!我从来没有想过。你说得很有道理。
  • 我对 StringBuffer 与 StringBuilder 的最大抱怨是它们与某些接口或抽象超类无关。
  • @MeBigFatGuy,使用 Appendable 和 CharSequence?
  • @santiagobasulto:共享 字符串 很好 - 但共享 StringBuilder/StringBuffer 是不常见的情况。
  • @Jon,是的,乔恩,这就是我的意思。多个线程获取相同的 StringBuffer 实例并使用它。
【解决方案2】:

StringBuffer 在 Java 1.0 中;这不是对缓慢或不变性的任何反应。它也不比字符串连接更快或更好。事实上,Java 编译器会编译

String s1 = s2 + s3;

变成类似的东西

String s1 = new StringBuilder(s2).append(s3).toString();

如果你不相信我,你自己用反汇编器试试吧(例如 javap -c)

关于“StringBuffer 比连接快”的事情是指重复 连接。在这种情况下,显式创建你自己的 StringBuffer 并重复使用它比让编译器创建许多它们执行得更好。

正如您所说,出于性能原因,Java 5 中引入了 StringBuilder。有意义的原因是 StringBuffer/Builder 实际上从不在创建它们的方法之外共享:它们的 99% 的使用类似于上面的内容,它们是在其中创建的,用于将几个字符串附加在一起,然后被丢弃。

【讨论】:

  • 你说得对,欧内斯特。它们都来自 1.0。我只是想说明一点。我已经对字符串连接(使用 + 运算符)与 StringBuffer.append() 进行了基准测试,并且速度确实更快。
  • 在引入 StringBuilder 之后,他们将编译器更改为使用 StringBuilder 而不是 StringBuffer 来实现 + 运算符。
  • @Paulo:的确,这是动机的主要部分!固定。
【解决方案3】:

如今,StringBuffer 和 Builder 都有些无用(从性能的角度来看)。 我解释一下原因:

StringBuilder 应该比 StringBuffer 更快,但任何理智的 JVM 都可以优化同步。因此,当它推出时,它是一个巨大的失误(和小成功)。

StringBuffer 在创建字符串时用于不复制 char[](在非共享变体中);然而,这是问题的主要来源,包括为小字符串泄漏巨大的 char[]。在 1.5 中,他们决定每次都必须生成 char[] 的副本,这实际上使 StringBuffer 无用(同步是为了确保没有线程游戏可以欺骗 String)。这节省了内存,但最终有助于 GC(除了明显减少的占用空间),通常 char[] 是消耗内存的对象的前 3 个。

String.concat 过去和现在都是连接 2 个字符串(并且只有 2 个......或者可能是 3 个)的最快方法。请记住,它不会执行 char[] 的额外副本。

回到没用的部分,现在任何 3rd 方代码都可以达到与 StringBuilder 相同的性能。即使在 java1.1 中,我曾经有一个类名 AsycnStringBuffer,它与 StringBuilder 现在所做的完全相同,但它仍然分配了比 StringBuilder 更大的 char[]。默认情况下,StrinBuffer/StringBuilder 都针对小字符串进行了优化,您可以看到 c-tor

  StringBuilder(String str) {
    super(str.length() + 16);
    append(str);
    }

因此,如果第二个字符串长于 16 个字符,它会得到另一个底层的副本 字符[]。很不酷。

这可能是在 32 位操作系统上尝试将 StringBuilder/Buffer 和 char[] 放入同一缓存行(在 x86 上)的副作用......但我不确定。

至于调试时间等的评论。使用你的判断,我个人不记得有任何关于字符串操作的问题,除了 impl。 JDO impl的sql生成器的ropelike结构。


编辑: 下面我说明了 java 设计者没有做些什么来使 String 操作更快。 请注意,该类适用于 java.lang 包,并且只能通过将其添加到引导类路径来放置。但是,即使不放在那里(区别是一行代码!),它仍然比 StringBuilder 快,令人震惊?该类会使 string1+string2+... 比使用 StringBuilder 好很多,但是很好...

package java.lang;

public class FastConcat {

    public static String concat(String s1, String s2){
        s1=String.valueOf(s1);//null checks
        s2=String.valueOf(s2);

        return s1.concat(s2);
    }

    public static String concat(String s1, String s2, String s3){
        s1=String.valueOf(s1);//null checks
        s2=String.valueOf(s2);
        s3=String.valueOf(s3);
        int len = s1.length()+s2.length()+s3.length();
        char[] c = new char[len];
        int idx=0;
        idx = copy(s1, c, idx);
        idx = copy(s2, c, idx);
        idx = copy(s3, c, idx);
        return newString(c);
    }
    public static String concat(String s1, String s2, String s3, String s4){
        s1=String.valueOf(s1);//null checks
        s2=String.valueOf(s2);
        s3=String.valueOf(s3);
        s4=String.valueOf(s4);

        int len = s1.length()+s2.length()+s3.length()+s4.length();
        char[] c = new char[len];
        int idx=0;
        idx = copy(s1, c, idx);
        idx = copy(s2, c, idx);
        idx = copy(s3, c, idx);
        idx = copy(s4, c, idx);
        return newString(c);

    }
    private static int copy(String s, char[] c, int idx){
        s.getChars(c, idx);
        return idx+s.length();

    }
    private static String newString(char[] c){
        return new String(0, c.length, c);
        //return String.copyValueOf(c);//if not in java.lang
    }
}

【讨论】:

  • 一些事情: 1. JVM 可以(AFAIK)只优化局部变量的同步(转义分析),所以当使用非局部变量时,StringBuilder 仍然比StringBuffer 快​​。 2. 使用StringBuilder documents 表示您不关心同步。如果使用得当,这会使代码更容易理解。 3. 您的帖子听起来好像StringBuilder 永远不会比串联快。您的concat 代码一切正常,但它解决的问题与StringBuilder 不同。我怀疑在有很多连接的紧密循环中它会更快。
  • 1)。不是真的 - 这取决于 JVM 内联多少,即对象可以逃脱多少,无竞争同步也接近免费,如今(它不会膨胀对象标题)。 2)这并不是真正的性能问题(我本人倾向于查看代码,主要是)。和 3)帖子没有这么说,提供示例代码以供您在获得>的情况下使用。 String s="xxx: "+someInt+" yyy"+anotherString;这将击败任何 StringBuilder ......回到正题。很多暗示。 java1.5 已经删除了 StringBuffer b/c 它对共享(超过 1 个)调用没有帮助。
  • @Konrad,也许尚不清楚:到目前为止,StringBuilder/Buffer 都从放入 java.lang 包中获得零好处。因此,自定义 impl。 form java1.1 的执行速度与 StringBuilder 一样快。在 java1.5 之前,StringBuffer 曾经尝试过巧妙地不复制 char[] 以避免分配(和一些次要的 GC)成本。没关系,但是不添加一些简单的 concat 方法(如上面的截图)并不是明智之举。对于 2 个字符串,String.concat 过去并且总是比 StringBuilder/Buffer 快。你甚至都无法靠近(除非 JVM 可以使用 intrisics)
【解决方案4】:

我在 XP 机器上尝试过同样的事情。 StringBuilder 稍微快一些,但是如果您颠倒运行顺序,或进行多次运行,您会注意到结果中的“几乎第二个因素”将变为 10% 的优势:

StringBuffer build & output duration= 4282,000000 µs
StringBuilder build & output duration= 4226,000000 µs
StringBuffer build & output duration= 4439,000000 µs
StringBuilder build & output duration= 3961,000000 µs
StringBuffer build & output duration= 4801,000000 µs
StringBuilder build & output duration= 4210,000000 µs

对于您的测试,JVM 不会提供帮助。我不得不限制运行和元素的数量,只是为了从“仅字符串”测试中获得任何结果。

【讨论】:

    【解决方案5】:

    决定通过一个简单的 XML 组合练习来测试这些选项。测试是在 2.7GHz i5 和 16Gb DDR3 RAM 上完成的,适合那些希望复制结果的人。

    代码:

       private int testcount = 1000; 
       private int elementCount = 50000;
    
       public void testStringBuilder() {
    
        long total = 0;
        int counter = 0;
        while (counter++ < testcount) {
            total += doStringBuilder();
        }
        float f = (total/testcount)/1000;
        System.out.printf("StringBuilder build & output duration= %f µs%n%n", f); 
    }
    
    private long doStringBuilder(){
        long start = System.nanoTime();
        StringBuilder buffer = new StringBuilder("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
        buffer.append("<root>");
          for (int i =0; i < elementCount; i++) {
              buffer.append("<data/>");
          }
          buffer.append("</root>");
         //System.out.println(buffer.toString());
          output = buffer.toString();
          long end = System.nanoTime();
         return end - start;
    }
    
    
    public void testStringBuffer(){
        long total = 0;
        int counter = 0;
        while (counter++ < testcount) {
            total += doStringBuffer();
        }
        float f = (total/testcount)/1000;
    
        System.out.printf("StringBuffer build & output duration= %f µs%n%n", f); 
    }
    
    private long doStringBuffer(){
        long start = System.nanoTime();
        StringBuffer buffer = new StringBuffer("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
        buffer.append("<root>");
          for (int i =0; i < elementCount; i++) {
              buffer.append("<data/>");
          }
          buffer.append("</root>");
         //System.out.println(buffer.toString());
          output = buffer.toString();
    
          long end = System.nanoTime();
          return end - start;
    }
    

    结果:

    在 OSX 机器上: StringBuilder 构建和输出持续时间= 1047.000000 µs StringBuffer 构建和输出持续时间= 1844.000000 µs 在 Win7 机器上: StringBuilder 构建和输出持续时间= 1869.000000 µs StringBuffer 构建和输出持续时间= 2122.000000 µs

    所以看起来性能增强可能是特定于平台的,取决于 JVM 如何实现同步。

    参考资料:

    此处介绍了 System.nanoTime() 的使用 -> Is System.nanoTime() completely useless? 和此处 -> How do I time a method's execution in Java?

    这里是 StringBuilder 和 StringBuffer 的来源 -> http://www.java2s.com/Open-Source/Java-Document/6.0-JDK-Core/lang/java.lang.htm

    这里有很好的同步概述 -> http://www.javaworld.com/javaworld/jw-07-1997/jw-07-hood.html?page=1

    【讨论】:

    • 感谢您的回答,但我不是在寻找基准。我的问题是关于 Java 平台的架构。无论如何,您应该真正看一下您的测试/基准,因为 StringBuilder 在单线程环境中应该更快(不想粗鲁,但这不是一个好的基准)。内存也是很好的测试,看看StringBuffer或StringBuilder是否使用了一些辅助结构。我建议您创建一个方法,因为“concat”方法对于所有类都是相同的,即:具有相同的 API。
    • 我同意 StringBuilder should 更快,这里的类似测试 -> littletutorials.com/2008/07/16/… 确实表明了这一点(也在 jre6 中完成)。问题是为什么——也许是一个潜在的架构原因?在具有 1Gb RAM 的 Windows 7 机器上进行进一步测试产生了预期的结果(即 StringBuilder 更快)(之前的测试是在 OSX 机器上进行的)......也许不同的实现以不同的方式利用可用内存,也许主机操作系统也起着重要作用?
    • 不,我朋友的原因是你的测试不好!如果你有时间阅读 Dive into Python 本章的介绍(这是 python,不是 Java,抱歉)。 diveintopython.net/performance_tuning/timeit.html
    • 感谢@santiagobasulto 指出单值测试毫无意义 - 应该知道得更好......但它看起来工作正常......嗯嗯......
    猜你喜欢
    • 2011-02-27
    • 1970-01-01
    • 1970-01-01
    • 2015-10-19
    • 1970-01-01
    • 1970-01-01
    • 2018-03-17
    • 2012-09-09
    相关资源
    最近更新 更多