【问题标题】:Why String's replaceAll() method come at high performance cost at the first time and faster at the next time?为什么 String 的 replaceAll() 方法第一次的性能成本很高,下一次的速度更快?
【发布时间】:2020-06-09 07:04:27
【问题描述】:

我了解到String's replaceAll() 方法将正则表达式作为输入参数,它会导致相当大的性能影响。但是有一次,我读到这个blog 用一个小程序断言(根据我的理解):

第一次处理replaceAll()方法很慢,但下一次更快。

这是测试结果:

regex replace time taken: 14.09 milliseconds
manual replace time taken: 2.371 seconds
-----
regex replace time taken: 9.498 milliseconds
manual replace time taken: 2.406 seconds
-----
regex replace time taken: 2.184 milliseconds
manual replace time taken: 2.360 seconds
-----

这个结果背后的优化机制是什么?

【问题讨论】:

    标签: java regex string optimization


    【解决方案1】:

    通常它不会造成有意义的性能影响,除非以奇怪和不寻常的方式使用。在一个正常的用例(比如说网络请求)中,它会在网络延迟和其他需要更多时间的事情下消失。只有在非常热的循环中使用replaceAll,才有必要考虑直接使用PatternMatcher 类,这有助于提高性能。

    链接的教程站点似乎有问题(其中有很多,这就是为什么您应该小心阅读内容的原因)。一方面,它将replaceAll 与编写得很差的手动替换方法进行比较(这就是为什么你会得到秒与毫秒的差异)。然后据此得出结论。

    所以链接中的结果背后没有优化机制。结果背后的原因是编写不好的手动替换方法,它连接了很多字符串,与replaceAll相比,速度很慢。

    【讨论】:

      【解决方案2】:

      以下摘自官方1 OpenJDK 11 源码2

      String.replaceAll 方法本身开始。

      public String replaceAll(String regex, String replacement) {
          return Pattern.compile(regex).matcher(this).replaceAll(replacement);
      }
      

      这里没有缓存。接下来Pattern.compile

      public static Pattern compile(String regex) {
          return new Pattern(regex, 0);
      }
      

      那里也没有缓存。而不是在私有 Pattern 构造函数中。

      Pattern 构造函数使用内部compile() 方法将正则表达式编译为内部形式。它采取措施避免Pattern 被编译两次。但是从上面可以看出,每个replaceAll调用都会生成一个单次使用的Pattern对象。

      那么,为什么您会看到这些性能数据有所提升?

      • 他们可能正在使用旧版本(Java 6 之前)Pattern,可能有3 个缓存的编译模式。

      • 最可能的解释是这只是 JVM 预热效应。一个编写良好的基准应该说明这一点,但该博客中使用的基准没有进行适当的预热。

      简而言之,你认为的一些“优化”导致的加速显然只是 JVM 预热效应的结果,例如 PatternMatcher 和相关类的 JIT 编译。


      1 - Java 6 以上版本的 OpenJDK 源代码可以从https://openjdk.java.net/下载

      2 - OpenJDK 6 源代码做同样的事情:没有缓存。

      3 - 我没有检查,但它没有实际意义。基于 EOL 版本的 Java 的性能基准对当前版本的 Java 没有指导意义。没有人应该仍然使用 Java 5。如果他们是,replaceAll 的性能是他们最不担心的。

      【讨论】:

      • The OpenJDK 6 source code is doing the same thing: no caching. >> 这个引用有官方文档吗? @斯蒂芬C
      • 源码本身是官方文档。我什至引用了它! AFAIK,没有官方的 specification 说明模式是否被缓存。实际实现(多个版本)的证据是没有缓存。 (至少,与此示例无关。)
      【解决方案3】:

      好吧,如果你一次又一次地在同一个表达式上执行 replaceAll,那么匹配的字符确实是更少的频率,所以更少的替换。

      示例:/(.)(.*?)\1/ 匹配带有重复字符的字符串,replaceAll 匹配 $2 将删除这些重复字符,但需要多次执行才能处理所有重复字符(有例如可以是 2 美元)

      • ABCDEFABCBCDEF 例如给出 BCDEFCCDEF
      • BCDEFCCDEF ==> BDEFCDEF
      • BDEFCDEF ==> BEFCEF
      • BEFCEF ==> BFCF
      • BFCF ==> 公元前

      处理后的字符串越来越小,匹配次数越来越少,处理时间也随之减少。

      我承认这个答案确实特定于用例,但我无法重现您提到的行为,因此它必须以某种方式特定于用例。

      缓存也可能是一个原因 - 我首先读取 14 而不是毫秒 - 但是像这样混合单位也不是一个好主意...

      【讨论】:

        【解决方案4】:

        好吧,replaceAll 在内部使用正则表达式,每次调用它时都会编译,但是如果我们一次又一次地在同一个表达式上执行 replaceAll,java 可能会使用一些内部机制,这样同一个表达式就不会再次编译,从而使重复替换更快,或者只是 JIT 效应。

        【讨论】:

          猜你喜欢
          • 2023-03-05
          • 2016-08-21
          • 1970-01-01
          • 1970-01-01
          • 2014-10-09
          • 1970-01-01
          • 2014-11-25
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多