【java】Java中的微优化--String.replaceAll

在这篇文章中,我们将讨论另一种流行的代码结构String.replaceAllString.replace方法的使用,我们将研究它如何影响 Java 11 中代码的性能以及您可以做些什么。

(请从性能的角度考虑以下所有代码)

(请不要关注数字,它们只是证明这一点的指标)

String.replaceAll

我不会说我喜欢编造示例,所以这次,我们将从 Spring 框架的现有代码库开始。我们看一下spring-messaging模块的MetadataEncoder类这一行

value.contains(".") ?value.replaceAll("\\.","%2E") :值;

你知道这里有什么问题吗?

这段代码试图做的就是对符号进行编码,然后将结果传递给 HTTP URL。我很幸运能在流行的代码库中找到那个特定的代码片段。它在一行中有几件事。

如果您是一位经验丰富的开发人员,您已经知道String.replaceAll 方法使用正则表达式模式作为第一个参数:

公共字符串替换所有(字符串正则表达式,字符串替换){
    返回模式。<em>编译</em>(正则表达式).matcher(this) 
                  .replaceAll(replacement); 
}

但是,在上面的代码中,我们只替换了字符。很多时候,当您执行替换操作时,您不需要任何模式匹配。就性能而言,您可以使用另一种非常相似且更轻的方法 - String.replace:

公共字符串替换(CharSequence 目标,CharSequence 替换)

例如,当您需要像我们的示例中那样替换单个单词或单个字符时。

注意:在 Java 8 中,String.replace方法 使用里面的Pattern作为String.replaceAll。但是,自 Java 9 以来,它发生了变化。这为我们在现有代码库中提供了很大的优化空间。

此外,String.replaceString.replaceAll方法的使用在设计上似乎很容易出错。这是因为当您开始在 IDE 中输入内容并仔细查看这两种方法时,您可能会认为String.replace仅替换第一个匹配项,而replaceAll替换所有匹配项。直观地,您将选择String.replaceAll而不是String.replace

另一个有趣的地方是上面的代码已经有了一个微优化,就是value.contains(“.”)方法的使用。因此,如果没有可替换的内容,则会检查字符串中的符号以避免模式匹配。

好的,让我们修复上面的示例:

 value.replace(".", "%2E");

我们还可以尝试应用String.indexOf(“.”)优化并检查这是否有助于Java 11 中使用String.replace方法:

 value.contains(".") ?value.replace(".", "%2E") : 值;

让我们编写基准测试:

@BenchmarkMode(Mode.AverageTime)
@Fork(1)
@State(Scope.Thread)
@Warmup(iterations = 5, time = 1)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Measurement(iterations = 10, time = 1)
public class ReplaceAll {

    @Param({"", "somePathNoDoT", "some.Path.With.Dot"})
    String value;

    @Benchmark
    public String replaceAllWithContains() {
        return value.contains(".") ? value.replaceAll("\\.", "%2E") : value;
    }

    @Benchmark
    public String replaceAll() {
        return value.replaceAll("\\.", "%2E");
    }

    @Benchmark
    public String replace() {
        return value.replace(".", "%2E");
    }

    @Benchmark
    public String replaceWithContains() {
        return value.contains(".") ? value.replace(".", "%2E") : value;
    }

}

结果(分数越低意味着越快):

JDK 11.0.8, OpenJDK 64-Bit Server VM, 11.0.8+10

```
Benchmark                                     (value)  Mode  Cnt    Score    Error  Units
ReplaceAll.replace                                     avgt   10    3.836 ±  0.060  ns/op
ReplaceAll.replaceAll                                  avgt   10  147.857 ±  1.550  ns/op
ReplaceAll.replaceAllWithContains                      avgt   10    3.777 ±  0.069  ns/op
ReplaceAll.replaceWithContains                         avgt   10    3.778 ±  0.075  ns/op

ReplaceAll.replace                      somePathNoDoT  avgt   10    7.647 ±  0.646  ns/op
ReplaceAll.replaceAll                   somePathNoDoT  avgt   10  156.495 ±  1.751  ns/op
ReplaceAll.replaceAllWithContains       somePathNoDoT  avgt   10    7.640 ±  0.291  ns/op
ReplaceAll.replaceWithContains          somePathNoDoT  avgt   10    7.545 ±  0.298  ns/op

ReplaceAll.replace                 some.Path.With.Dot  avgt   10  123.856 ±  1.686  ns/op
ReplaceAll.replaceAll              some.Path.With.Dot  avgt   10  389.632 ± 18.308  ns/op
ReplaceAll.replaceAllWithContains  some.Path.With.Dot  avgt   10  378.527 ±  6.434  ns/op
ReplaceAll.replaceWithContains     some.Path.With.Dot  avgt   10  126.918 ±  0.940  ns/op
```

看起来String.indexOf(“.”)String.replaceAll方法一起使用,实际上是有道理的,即使对于空输入字符串,编译和匹配模式也需要太多时间。约 50 倍的差异是巨大的。这同样适用于没有任何点符号的输入字符串。而当必须执行实际的替换工作时,String.replace方法的性能比String.replaceAll 高出三倍

此外,似乎String.indexOf优化对Java 11 中的String.replace方法没有任何意义,而在 Java 8 中当我们内部有模式匹配时需要它。现在它甚至让它变慢了一点。这是因为String.replace方法的实现,因为它已经在里面执行了String.indexOf搜索。所以我们在这里做双重工作。

“但是你可以预编译正则模式表达式并使用它来提高String.replaceAll方法的性能”,你会说。同意。事实上,我们在Blynk经常这样做。

让我们检查预编译模式如何更改数字:

@BenchmarkMode(Mode.AverageTime)
@Fork(1)
@State(Scope.Thread)
@Warmup(iterations = 5, time = 1)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Measurement(iterations = 10, time = 1)
public class ReplaceAllPrecompiled {

    @Param({"", "somePathNoDoT", "some.Path.With.Dot"})
    String value;
    private static final Pattern DOT_PATTERN = Pattern.compile("\\.");

    @Benchmark
    public String replaceAllWithPrecompiled() {
        return DOT_PATTERN.matcher(value).replaceAll("%2E");
    }

}

结果(分数越低意味着越快):

JDK 11.0.8, OpenJDK 64-Bit Server VM, 11.0.8+10

```
Benchmark                         (value)  Mode  Cnt    Score    Error  Units
replace                                    avgt   10    3.843 ±  0.100  ns/op
replace                     somePathNoDoT  avgt   10    8.693 ±  0.693  ns/op
replace                some.Path.With.Dot  avgt   10  127.022 ±  8.842  ns/op

replaceAll                                 avgt   10  156.550 ± 10.741  ns/op
replaceAll                  somePathNoDoT  avgt   10  159.619 ±  4.654  ns/op
replaceAll             some.Path.With.Dot  avgt   10  391.912 ± 11.445  ns/op

replaceAllPrecompiled                      avgt   10   52.466 ±  0.791  ns/op
replaceAllPrecompiled       somePathNoDoT  avgt   10   64.337 ±  0.854  ns/op
replaceAllPrecompiled  some.Path.With.Dot  avgt   10  274.867 ±  3.535  ns/op

是的,现在数字好多了。但是,仍然不如String.replace方法那么好。我们可以尝试更多地专注于优化正则模式,但是关于它的帖子已经足够多。

您可能会认为最初的示例只是一个地方,而且非常罕见。让我们看看 GitHub:

GitHub 刚刚索引了一些存储库,在第一个屏幕上,六个String.replaceAll用法中有五个可以替换为String.replace!是的,许多项目仍在使用 Java 8,这对它们没有任何影响。但是,在大多数开发人员迁移到 Java 11 之后,我们将有很多缓慢的遗留代码。我们可以立即开始改进它。

StringUtils.replace

在 Java 11 之前,当您有使用String.replace方法的热门路径时,您必须在一些 3-d 方库中找到更快的替代方案,甚至编写自己的自定义版本。最著名的 3-d 方替代方案是 Apache Commons StringUtils.replace方法。

例如,可以在 Spring 框架中找到自定义替换方法的示例。在这里

让我们看另一个 Spring代码片段

字符串内部名称 = StringUtils 替换(类名,“。”,“/”);

你知道这里有什么问题吗?

让我们在基准测试中检查 Spring(最新源代码)、Apache Commons(commons-lang3 的最新版本 3.11)和 Java 方法:

@BenchmarkMode(Mode.AverageTime)
@Fork(1)
@State(Scope.Thread)
@Warmup(iterations = 5, time = 1)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Measurement(iterations = 10, time = 1)
public class ReplaceCustom {

    @Param({"", "somePathNoDoT", "some.Path.With.Dot"})
    String value;

    @Benchmark
    public String replace() {
        return value.replace(".", "/");
    }

    @Benchmark
    public String replaceSpring() {
        return springReplace(value, ".", "/");
    }

    @Benchmark
    public String replaceApache() {
        return StringUtils.replace(value, ".", "/");
    }
}

结果(分数越低意味着越快):

JDK 11.0.8, OpenJDK 64-Bit Server VM, 11.0.8+10

```
Benchmark                                     (value)  Mode  Cnt    Score   Error  Units
ReplaceCustom.replace                                  avgt   10    4.497 ± 1.085  ns/op
ReplaceCustom.replace                   somePathNoDoT  avgt   10    7.944 ± 0.289  ns/op
ReplaceCustom.replace              some.Path.With.Dot  avgt   10  106.270 ± 5.095  ns/op

ReplaceCustom.replaceApache                            avgt   10    3.784 ± 0.065  ns/op
ReplaceCustom.replaceApache             somePathNoDoT  avgt   10    8.168 ± 0.052  ns/op
ReplaceCustom.replaceApache        some.Path.With.Dot  avgt   10  121.248 ± 1.935  ns/op

ReplaceCustom.springCustomReplace                      avgt   10    3.628 ± 0.058  ns/op
ReplaceCustom.springCustomReplace       somePathNoDoT  avgt   10    7.991 ± 0.268  ns/op
ReplaceCustom.springCustomReplace  some.Path.With.Dot  avgt   10  108.191 ± 6.138  ns/op

嗯,看起来所有方法都非常接近。Apache Commons 有点慢,但那是因为它有额外的逻辑来处理不区分大小写的替换。所以一切都是有道理的。

现在,由于我们具有相似的性能,我们不再需要自定义方法或 3-d 方库来执行Java 11 中的快速String.replace

但是这条线还是不行:

返回值.replace(".", "/");

你知道这里有什么问题吗?

与发生实际字符串替换的第一个示例相反,这里我们有一个用于搜索和替换的字符。众所周知,Java 有一个专门的字符替换版本:

字符串替换(char oldChar, char newChar)

让我们也将它添加到我们的基准测试中:

@Benchmark 
public String replaceChar() { 
    return value.replace('.', '/'); 
}

Apache Commons 库也有StringUtils.replaceChars,但它在内部使用String.replace(char, char),所以我们将跳过它。我们离从您的项目中消除这个 3-d 派对库更近了一步。

结果(分数越低意味着越快):

JDK 11.0.8, OpenJDK 64-Bit Server VM, 11.0.8+10

```
Benchmark                                     (value)  Mode  Cnt    Score   Error  Units
ReplaceCustom.replace                                  avgt   10    4.497 ± 1.085  ns/op
ReplaceCustom.replace                   somePathNoDoT  avgt   10    7.944 ± 0.289  ns/op
ReplaceCustom.replace              some.Path.With.Dot  avgt   10  106.270 ± 5.095  ns/op

ReplaceCustom.replaceChar                              avgt   10    3.689 ± 0.040  ns/op
ReplaceCustom.replaceChar               somePathNoDoT  avgt   10    8.787 ± 0.119  ns/op
ReplaceCustom.replaceChar          some.Path.With.Dot  avgt   10   26.824 ± 0.391  ns/op

ReplaceCustom.springCustomReplace                      avgt   10    3.628 ± 0.058  ns/op
ReplaceCustom.springCustomReplace       somePathNoDoT  avgt   10    7.991 ± 0.268  ns/op
ReplaceCustom.springCustomReplace  some.Path.With.Dot  avgt   10  108.191 ± 6.138  ns/op

用于单个字符替换的 Java 字符专用版本比重载的String.replace(String, String)和自定义 Spring方法快四倍。有趣的是,即使在 Java 8 中,String.replace(char, char)的优化也足够好。所以 Spring 可以安全地使用String.replace(char, char)

字符串.remove

我希望你还不累:)?让我们看一下最后一个代码示例:

 value.replace(".", "");

你知道这里有什么问题吗?

不幸的是,Java 仍然没有String.remove方法。作为替代方案,我们可以使用String.replace(char, char)方法,但 Java 也没有空字符文字,我们不能编写这样的代码:

value.replace('.', '');

所以,相反,我们必须使用上面的“hack”。

幸运的是,有许多 3-d 方实现,例如 Apache Commons StringUtils.remove(String, char)。例如,Spring 使用自己的自定义实现,基于自己的自定义替换方法:

public static String delete(String inString, String pattern) { 
   return StringUtils. <em>替换</em>(inString,模式,“”);
}

让我们再次检查我们的基准测试中的 Spring、Apache Commons 和 Java 方法以进行删除操作:

@BenchmarkMode(Mode.AverageTime)
@Fork(1)
@State(Scope.Thread)
@Warmup(iterations = 5, time = 1)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Measurement(iterations = 10, time = 1)
public class RemoveChar {

    @Param({"", "somePathNoDoT", "some.Path.With.Dot"})
    String value;

    @Benchmark
    public String remove() {
        return value.replace(".", "");
    }

    @Benchmark
    public String removeApache() {
        return StringUtils.remove(value, '.');
    }

    @Benchmark
    public String removeSpring() {
        return springDelete(value, ".");
    }   
}

结果(分数越低意味着越快):

JDK 11.0.8, OpenJDK 64-Bit Server VM, 11.0.8+10

```
Benchmark                           (value)  Mode  Cnt   Score   Error  Units
RemoveChar.remove                            avgt   10   4.405 ± 1.646  ns/op
RemoveChar.remove             somePathNoDoT  avgt   10   7.820 ± 0.296  ns/op
RemoveChar.remove        some.Path.With.Dot  avgt   10  92.185 ± 3.752  ns/op

RemoveChar.removeApache                      avgt   10   3.708 ± 0.070  ns/op
RemoveChar.removeApache       somePathNoDoT  avgt   10  10.018 ± 0.103  ns/op
RemoveChar.removeApache  some.Path.With.Dot  avgt   10  37.822 ± 0.486  ns/op

RemoveChar.removeSpring                      avgt   10   3.609 ± 0.033  ns/op
RemoveChar.removeSpring       somePathNoDoT  avgt   10   7.996 ± 0.308  ns/op
RemoveChar.removeSpring  some.Path.With.Dot  avgt   10  87.148 ± 1.391  ns/op
```

专门的 Apache Commons 版本几乎了三倍。这里有趣的是,即使是专门和优化的 char 删除也比String.replace(char, char)中的 char 替换要慢。肯定还有进一步改进的空间。

希望有一天我们会在 Java 中看到这一点。

结论

  • 使用 Java 11
  • 尽可能在String.replaceAll 上使用 S string.replace
  • 如果必须使用String.replaceAll,请尝试在热路径中预编译正则表达式
  • 用的专门版本,请与string.replace(字符,字符),而不是与string.replace(字符串,字符串)时,你可以
  • 对于热路径,您仍然需要考虑 3-d 方库或自定义方法,而不是String.replace(value, “”)代码模式

这是基准测试的源代码,您可以自己尝试。

感谢您的关注,敬请期待。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值