【问题标题】:Compile time optimization of String concatenation字符串拼接的编译时优化
【发布时间】:2018-02-10 02:15:10
【问题描述】:

我很好奇,下面代码sn-p的优化能走多远。

据我所知,每当 StringBuffer 的容量扩展时,都会消耗一些 CPU 工作,因为它的内容需要重新分配。不过,我猜 Java 编译器优化可以预先计算所需的容量,而不是进行多次重新分配。

问题是:下面的sn-p代码会这样优化吗?

public static String getGetRequestURL(String baseURL, Map<String, String> parameters) {
    StringBuilder stringBuilder = new StringBuilder();
    parameters.forEach(
            (key, value) -> stringBuilder.append(key).append("=").append(value).append("&"));
    return baseURL + "?" + stringBuilder.delete(stringBuilder.length(),1);
}

【问题讨论】:

  • 如何预分配?编译器在运行时对parameters 中的键和值的大小有什么了解?
  • StringBuilder 直接由 char 数组支持。如果您使用StringBuilder stringBuilder = new StringBuilder(""); 代替StringBuilder stringBuilder = new StringBuilder("");,您会发现开销减少了,因为StringBuilder 会将内部缓冲区预分配给256 元素,从而减少需要动态调整缓冲区大小的可能性(缓冲区的大小和内容的大小是构建器自己管理的两种不同状态)
  • 不能Java编译器优化让它遍历集合并计算结果字符串的总大小以避免在最坏情况下重新分配的成本吗?
  • @DmitriiDemenev 怎么样? Map 的内容是在运行时定义的
  • @DmitriiDemenev 在这种情况下,我会说,不,Javac 不会执行任何额外的优化来预先计算内容的大小

标签: java optimization string-concatenation


【解决方案1】:

在 Java 中,大多数优化是由运行时的即时编译器执行的,因此通常 javac 优化并不重要。

因此,Java 编译器不需要优化字符串连接,尽管只要不涉及循环,所有编译器都倾向于这样做。您可以使用 javap(JDK 中包含的 java 反编译器)检查此类编译时优化的程度。

那么,javac 可以想象优化这个吗?要确定字符串生成器的长度,它必须对映射进行两次迭代。由于 java 没有 const 引用,并且编译器对Map 没有特殊处理,编译器无法确定这种重写是否会保留代码的含义。即使可以,也不清楚这些收益是否值得两次迭代的成本。毕竟,现代处理器可以在一条 cpu 指令中复制 4 到 8 个字符。由于内存访问是顺序的,因此在增长缓冲区时不会丢失任何缓存。另一方面,第二次迭代映射可能会导致额外的缓存未命中,因为映射条目(以及它们引用的字符串)可能分散在整个主内存中。

无论如何,我不会担心这段代码的效率。即使您的 URL 有 1000 个字符长,调整缓冲区大小也需要大约 0.1 微秒。除非您有证据表明这确实是一个性能热点,否则您最好将时间花在其他地方。

【讨论】:

    【解决方案2】:

    首先:

    • 您可以通过使用 javap 工具查看字节码来了解 (javac) 编译时优化发生了什么。

    • 您可以通过让 JVM 转储本机代码来了解执行了哪些 JIT 编译器优化。

    因此,如果您出于实际原因需要了解您的代码是如何优化的(在特定平台上),那么您应该检查一下。


    实际上,javac 的优化非常简单,并没有达到预先计算缓冲区大小的程度。我还没有检查过,但我希望 JIT 编译器也是如此。我怀疑它是否会尝试使用“最佳”大小预先分配 StringBuilder

    为什么?

    原因包括以下几点:

    • 不准确的预先计算(平均而言)无济于事,而且可能比什么都不做更糟糕。
    • 准确的预计算通常涉及测量要连接的实际字符串的(动态)长度。
    • 实现优化逻辑会很复杂,并且会使优化器变慢并且维护起来更费力。
    • 在运行时,字符串测量会引入开销。很难确定您是否会经常出人头地以有所作为。 (人们不喜欢让代码运行速度变慢的优化...)
    • 有比使用字符串连接更好(更有效)的方法来进行大规模文本组装。程序员(对问题域和应用程序逻辑有更多了解)可以比编译器更好地优化它。如果将开发人员的精力花在这方面足够重要的话。

    【讨论】:

      【解决方案3】:

      一种优化是在stringBuilder中设置baseURL和&,而不是在末尾使用String concatenate,如:

      public static String getGetRequestURL(String baseURL, Map<String, String> parameters) {
        StringBuilder stringBuilder = new StringBuilder(baseURL);
      
        stringBuilder.append("&");
      
        parameters.forEach((key, value) -> stringBuilder.append(key).append("=").append(value).append("&"));
      
        stringBuilder.setLength(stringBuilder.length() - 1);
        return stringBuilder.toString();
      }
      

      如果您想要更快的速度,并且由于 javac 或 JIT 不会优化潜在的字符串大小,您可以自己跟踪它而不会产生太多开销,但添加一个最大大小跟踪器,例如:

      protected static URL_SIZE = 256;
      
      public static String getGetRequestURL(String baseURL, Map<String, String> parameters) {
        StringBuilder stringBuilder = new StringBuilder(URL_SIZE);
      
        stringBuilder.append(baseURL);
        stringBuilder.append("&");
      
        parameters.forEach((key, value) -> stringBuilder.append(key).append("=").append(value).append("&"));
      
        int size = stringBuilder.length();
        if (size > URL_SIZE) {
          URL_SIZE = size;
        }
      
        stringBuilder.setLength(size - 1);
        return stringBuilder.toString();
      }
      

      也就是说,通过对 100 万次调用的一些测试,我发现不同的版本执行为(以毫秒为单位):

      • 您的版本:总计 = 1151,平均 = 230
      • 高于版本 1:总计 = 936,平均 = 187
      • 高于版本 2:总数 = 839,平均 = 167

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2013-06-07
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-05-16
        • 2012-05-23
        • 2019-10-18
        • 2013-06-23
        相关资源
        最近更新 更多