【问题标题】:Is String concatenation on assignment efficient?分配时的字符串连接是否有效?
【发布时间】:2019-07-05 02:41:38
【问题描述】:

我知道使用“+”连接运算符来构建字符串是非常低效的,所以推荐使用StringBuilder类,但是我想知道这种模式是否也低效?

String some = a + "\t" + b + "\t" + c + "\t" + d + "\t" + e;

我猜这里编译器会优化赋值,还是不行?

【问题讨论】:

  • 没关系。编译器可以对其进行优化。另见How is String concatenation implemented in Java 9?
  • 我会谨慎地说@khelwood,我们不知道a/b/etc 都是常量(这是它优化为文字的地方)。否则,我们可能会在这里看到的唯一优化是编译器会自行将串联与StringBuilder 全部交换。在一个循环中,可能会解决很多不必要的字符串生成器。
  • 这取决于 a,b,c,d 是否为常量......如果编译器可以解析这些值(在编译时),编译器将对其进行优化
  • 为什么它们必须是常量?为什么正常变量会有问题? StringBuilder 也可以很好地处理变量,对吧。其他一些优化可能不起作用,但我们在谈论什么样的优化呢?
  • “优化为文字”不在问题陈述中。编译器将优化字符串连接以在涉及非常量项的单个表达式中调用StringBuilder.append。正如所指出的,它不会在不同的行上进行连接,尽管 JIT/HotSpot 运行时编译器可能会做一些事情。

标签: java string java-8 string-concatenation


【解决方案1】:

这个特定的例子将被编译器内联:

String a = "a";
String b = "bb";
String c = "ccc";
String some = a + "\t" + b + "\t" + c;

Java 9+ 将使用 invokedynamic with makeConcatWithConstants 将其内联,使其高效。根据javap -v 输出:

Code:
  stack=3, locals=5, args_size=1
     0: ldc           #2                  // String a
     2: astore_1
     3: ldc           #3                  // String bb
     5: astore_2
     6: ldc           #4                  // String ccc
     8: astore_3
     9: aload_1
    10: aload_2
    11: aload_3
    12: invokedynamic #5,  0              // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
    17: astore        4
    19: return

但是如果abc是编译时常量编译器会进一步优化代码:

final String a = "a";
final String b = "bb";
final String c = "ccc";
String some = a + "\t" + b + "\t" + c;

some 将加载一个常量值:

Code:
  stack=1, locals=5, args_size=1
     0: ldc           #2                  // String a
     2: astore_1
     3: ldc           #3                  // String bb
     5: astore_2
     6: ldc           #4                  // String ccc
     8: astore_3
     9: ldc           #5                  // String a\tbb\tccc
    11: astore        4
    13: return

在其他情况下,例如for 循环编译器可能无法生成优化代码,因此 StringBuilder 可能更快。

【讨论】:

  • 想再次强调这仅适用于文字/常量连接。如果a/etc 是一个被传入的变量,我们将失去优化(根据我过去看到的情况,它可能会被StringBuilder 调用替换)。跨度>
  • @KarolDowbecki 我不知道这是如何回答这个问题的。 StringConcatFactory 只有两种方法:makeConcatmakeConcatWithConstants。现在这些方法将在运行时实际使用的是完全不同的故事。
  • @Rogue 你如何尝试将a 作为参数传递给方法并反编译它?它仍然是makeConcatWithConstants...
【解决方案2】:

在一般情况下,使用+ 和使用StringBuilder 进行字符串连接是绝对正确且有效的。但是在不同的情况下,使用+ 的连接效率低于使用StringBuilder

字符串连接不在循环中 - 高效!!!

这会产生良好的性能,因为 JVM 使用 StringBuilder 对其进行转换。

String some = a + "\t" + b + "\t" + c + "\t" + d + "\t" + e;

这样就OK了,因为JVM内部把这段代码改成下面这样:

String some = new StringBuilder().append(a).append('\t').append(c).append('\t')
                                 .append(d).append('\t').append(e).toString();

P.S. StringBuilder 有内部缓冲区 char[]。如果您知道结果字符串的长度,那么最好在一开始就保留整个缓冲区。例如。如果最终字符串 最多 1024 个字符,那么你可以这样做 new StringBuilder(1024)

循环中的字符串连接 - 效率不高!!!

这会导致性能下降,因为 JVM 无法用一个 StringBuilder 包装 while 循环,如下所示:

StringBuilder buf = new StringBuilder();

for (int i = 0; i < 10; i++)
    buf.append(a).append('\t').append(c).append('\t')
       .append(d).append('\t').append(e).append('t');

String some = buf.toString();

但 JVM 仍然能够优化每个循环迭代中的所有连接;像这样:

String some = "";

for (int i = 0; i < 10; i++) {
    some = new StringBuilder(some).append(a).append('\t').append(c).append('\t')
                               .append(d).append('\t').append(e).append('t');
}

如您所见,在循环中使用字符串连接有一些缺点。

【讨论】:

    【解决方案3】:

    您的前提“使用“+”连接运算符构建字符串效率非常低下”是不正确的。首先,字符串连接本身并不是一个廉价的操作,因为它意味着创建一个包含所有连接字符串的新字符串,因此需要复制字符内容。但这始终适用,无论如何你这样做。

    当您使用 + 运算符时,您是在说明您想做什么,而不是说明如何去做。甚至 Java 语言规范都不需要特定的实现策略,除非编译时常量的连接必须在编译时完成。因此,对于编译时常量,+ 运算符最有效的解决方案¹。

    实际上,从 Java 5 到 Java 8 的所有常用编译器都在底层使用 StringBuilder 生成代码(在 Java 5 之前,它们使用 StringBuffer)。这适用于像您这样的语句,因此用手动 StringBuilder 替换它不会有太大收获。通过提供合理的初始容量,您可能会比典型的编译器生成的代码稍微好一点,但仅此而已。

    从 Java 9 开始,编译器生成 invokedynamic 指令,允许运行时提供执行连接的实际代码。这可能是一个类似于过去使用的StringBuilder 代码,但也可能是完全不同的代码。最值得注意的是,运行时提供的代码可以访问应用程序代码无法访问的实现特定功能。所以现在,通过+ 的字符串连接甚至可以比基于StringBuilder 的代码更快。

    由于这仅适用于单个连接表达式,因此在使用多个语句甚至循环执行字符串构造时,在整个构造过程中始终使用StringBuilder 可能比多个连接操作更快。但是,由于代码在优化环境中运行,JVM 可以识别其中的一些模式,因此不能肯定地说。

    现在是时候记住旧规则了,只有在性能出现实际问题时才尝试优化性能。并且始终使用公正的测量工具来验证尝试的优化是否真正提高了性能。关于性能优化技巧,有很多广为流传的错误或过时的神话。

    ¹除非您有重复的部分并希望减小类文件的大小

    【讨论】:

    • 谢谢!我没有时间发布答案,因为我真的不喜欢接受的答案,而且我也不知道如何说在字节码级别有一个 StringConcatFactory::makeConcatWithConstants 实际回答它。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2010-09-30
    • 2011-11-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多