【问题标题】:How much does Java optimize string concatenation with +?Java 用 + 优化了多少字符串连接?
【发布时间】:2017-03-09 03:12:53
【问题描述】:

我知道在最近的 Java 版本中字符串连接

String test = one + "two"+ three;

将得到优化以使用StringBuilder

但是,每次遇到此行时会生成一个新的StringBuilder,还是会生成一个线程本地 StringBuilder,然后用于所有字符串连接?

换句话说,我是否可以通过创建自己的线程本地 StringBuilder 以重用来提高频繁调用方法的性能,或者这样做不会有显着的收益?

我可以为此编写一个测试,但我想知道它是否可能是编译器/JVM 特定的或者可以更普遍地回答的问题?

【问题讨论】:

  • 连接表达式时要小心重入。
  • 上次我检查它很愚蠢,迫使StringBuilder 反复重新分配。但这是特定于 Oracle 的 JDK,查看生成的字节码,因此没有考虑 JVM 可能进行的任何优化。我的规则是:99.999% 的时间你不在乎,当然;对于您关心的 0.001%,使用显式分配的 StringBuilder 足够大以处理总结果。
  • 除非你做的字符串操作比那一行多得多,否则我同意 T.J.:99.999% 的时间你不会看到任何区别。 JVM 实际上会将所有内存作为本地线程分配给一个线程(直到它需要与另一个线程共享),iiuc,所以你的线程本地可能不会有任何好处。
  • 如果它在像“a”+“b”+“c”这样的单一语句上,我相信它预先分配了正确的数量。一般来说,它会很好用。手动使用 StringBuffer/Builder 的情况是当您在循环中附加到单个字符串时——Java 将重复创建和销毁构建器并垃圾收集不太好的中间字符串。
  • 顺便说一句,引用的问题不是这个问题的一个很好的替代品,因为在搜索“优化字符串 +”时,你永远不会想出另一个问题,但是那里有重复的问题- -你至少应该找到stackoverflow.com/questions/1532461/…

标签: java string optimization stringbuilder string-concatenation


【解决方案1】:

据我所知,没有编译器生成代码重用 StringBuilder 实例,最值得注意的是 javac 和 ECJ 不生成重用代码。

需要强调的是,不进行此类重复使用是合理的。假设从 ThreadLocal 变量中检索实例的代码比从 TLAB 中的普通分配更快是不安全的。即使尝试添加本地 gc 循环以回收该实例的潜在成本,但就我们可以确定其在成本中的比例而言,我们无法得出结论。

因此,尝试重用构建器的代码会更加复杂,浪费内存,因为它使构建器保持活动状态而不知道它是否会被实际重用,没有明显的性能优势。

特别是当我们在上面的陈述之外考虑这一点时

  • 像 HotSpot 这样的 JVM 具有逃逸分析,可以完全省去像这样的纯本地分配,还可以省去数组调整大小操作的复制成本
  • 如此复杂的 JVM 通常还具有专门针对基于 StringBuilder 的连接的优化,当编译的代码遵循通用模式时效果最佳

使用 Java 9,情况将再次发生变化。然后,字符串连接将被编译为 invokedynamic 指令,该指令将在运行时链接到 JRE 提供的工厂(请参阅 StringConcatFactory)。然后,JRE 将决定代码的外观,如果它对特定的 JVM 有好处,则允许将其定制到特定的 JVM,包括缓冲区重用。这也将减少代码大小,因为它只需要一条指令而不是分配序列和多次调用StringBuilder

【讨论】:

  • 使用 jdk-9,图片再次戏剧性地改变 :)
【解决方案2】:

您会惊讶于 jdk-9 字符串连接付出了多少努力。首先 javac 发出一个invokedynamic 而不是对StringBuilder#append 的调用。该 invokedynamic 将返回一个 CallSite ,其中包含一个 MethodHandle(实际上是一系列 MethodHandle)。

因此,对字符串连接实际执行什么操作的决定移至运行时。缺点是第一次连接字符串时会变慢(对于相同类型的参数)。

那么在连接一个String的时候有一系列的策略可以选择(可以通过java.lang.invoke.stringConcat参数覆盖默认的):

private enum Strategy {
    /**
     * Bytecode generator, calling into {@link java.lang.StringBuilder}.
     */
    BC_SB,

    /**
     * Bytecode generator, calling into {@link java.lang.StringBuilder};
     * but trying to estimate the required storage.
     */
    BC_SB_SIZED,

    /**
     * Bytecode generator, calling into {@link java.lang.StringBuilder};
     * but computing the required storage exactly.
     */
    BC_SB_SIZED_EXACT,

    /**
     * MethodHandle-based generator, that in the end calls into {@link java.lang.StringBuilder}.
     * This strategy also tries to estimate the required storage.
     */
    MH_SB_SIZED,

    /**
     * MethodHandle-based generator, that in the end calls into {@link java.lang.StringBuilder}.
     * This strategy also estimate the required storage exactly.
     */
    MH_SB_SIZED_EXACT,

    /**
     * MethodHandle-based generator, that constructs its own byte[] array from
     * the arguments. It computes the required storage exactly.
     */
    MH_INLINE_SIZED_EXACT
}

默认策略是:MH_INLINE_SIZED_EXACT 这是一个野兽!

它使用包私有构造函数来构建字符串(这是最快的):

/*
 * Package private constructor which shares value array for speed.
 */
String(byte[] value, byte coder) {
    this.value = value;
    this.coder = coder;
}

首先,此策略创建所谓的过滤器;这些基本上是将传入参数转换为字符串值的方法句柄。正如人们所预料的那样,这些 MethodHandle 存储在一个名为 Stringifiers 的类中,在大多数情况下会产生一个调用以下方法的 MethodHandle:

String.valueOf(YourInstance)

因此,如果您有 3 个要连接的对象,则将有 3 个 MethodHandles 委托给String.valueOf(YourObject),这实际上意味着您已将对象转换为字符串。 这个类里面有一些我仍然无法理解的调整;比如需要有单独的类StringifierMost(只转换为字符串引用、浮点和双精度)和StringifierAny

由于MH_INLINE_SIZED_EXACT 表示字节数组被计算为精确大小;有一种计算方法。

这是通过StringConcatHelper#mixLen 中的方法完成的,这些方法采用输入参数的字符串化版本(References/float/double)。至此,我们知道了最终字符串的大小。好吧,我们实际上并不知道,我们有一个 MethodHandle 来计算它。

String jdk-9 中还有一个值得一提的变化——添加了coder 字段。这是计算字符串的大小/相等/字符所必需的。由于大小需要它,我们也需要计算它;这是通过StringConcatHelper#mixCoder 完成的。

此时委派一个 MethodHandle 来创建你的数组是安全的:

    @ForceInline
    private static byte[] newArray(int length, byte coder) {
        return (byte[]) UNSAFE.allocateUninitializedArray(byte.class, length << coder);
    }

每个元素是如何附加的?通过StringConcatHelper#prepend中的方法。

现在我们需要调用带字节的 String 的构造函数所需的所有细节。


所有这些操作(以及为简单起见跳过的许多其他操作)都是通过发出 MethodHandle 来处理的,该方法将在实际发生附加时调用。

【讨论】:

  • 我在这个答案中有点得意忘形,原因很简单,这样一个简单操作的细节很吸引 IMO。
  • 这真的很有趣,虽然不幸的是它并没有真正直接回答这个问题 - 所以我觉得我不能把勾号移过去:(
  • @TimB 完全同意,这与勾号无关:)。接受的答案是正确的。
  • 准确地说,CallSitebootstrap方法返回的对象,它封装了MethodHandleinvokedynamic指令得到链接到。 invokedynamic 指令返回,当然是String。这里仅隐含提及的是,String 现在使用byte[] 数组,` 每个字符仅使用一个字节对 iso-latin-1 字符串进行编码,仅此一项就已经将大多数字符串构造所需的数据移动减半。
猜你喜欢
  • 2011-06-12
  • 2010-09-22
  • 2010-09-22
  • 2017-06-10
  • 1970-01-01
  • 2010-09-29
  • 2011-08-20
  • 1970-01-01
相关资源
最近更新 更多