点击上方 果汁简历 ,选择“置顶公众号”
优质文章,第一时间送达
文章目录
- String.replace vs StringUtils.replace
- 什么是 JMH
- 使用 JMH 测试 replace
- JMH 基本概念
- 源码 & 课后题
String.replace vs StringUtils.replace
字符串的 replace
是我们平时最常用的操作了,那么你用对了吗?我们下面就快速的比较一下 String.replace
和 StringUtils.replace
的性能,你就会发现平时用的对不对了。
Benchmark Mode Cnt Score Error Units
longString1Match thrpt 21 1065385.376 ± 163542.395 ops/s
longString1MatchUtils thrpt 21 5796658.817 ± 402075.454 ops/s
longStringNMatch thrpt 21 836951.534 ± 184212.932 ops/s
longStringNMatchUtils thrpt 21 2604916.198 ± 1151573.761 ops/s
longStringNoMatch thrpt 21 2664613.208 ± 812092.909 ops/s
longStringNoMatchUtils thrpt 21 13064807.201 ± 5216438.357 ops/s
shortString1Match thrpt 21 1666278.062 ± 847582.653 ops/s
shortString1MatchUtils thrpt 21 8487720.328 ± 4039195.570 ops/s
shortStringNMatch thrpt 21 2160392.894 ± 326624.549 ops/s
shortStringNMatchUtils thrpt 21 7579329.122 ± 1555644.322 ops/s
shortStringNoMatch thrpt 21 4644501.698 ± 1052814.151 ops/s
shortStringNoMatchUtils thrpt 21 92366435.842 ± 31333158.039 ops/s
上面的内容是通过 JMH
输出的基准测试比较,我们可以清楚的看到 String.replace
和 StringUtils.replace
的差距是非常大,尤其是没有匹配项的时候,我们可以简单通过源码查看其原因,StringUtils
在替换的时候会优先判断是否有匹配然后再循环,这样减少了很多无用的操作,当然 JDK9
以后也做了这个功能的优化,所以如果你使用的是 9+ 可以忽略这个性能问题。
int end = searchText.indexOf(searchString, start);
if (end == INDEX_NOT_FOUND) {
return text;
}
其次呢,JDK 的 replace 里面使用的正则也是一个非常耗时测操作。
return Pattern
.compile(target.toString(), Pattern.LITERAL)
.matcher(this)
.replaceAll(Matcher.quoteReplacement(replacement.toString()));
好了说到这里基本知道平时替换字符串怎么做了吧,这个可不是大题小做,正所谓不积跬步无以至千里,那么接下来就来说说我们上文中的测试是怎么实现的。
什么是 JMH
Java Microbenchmark Harness, 由JIT开发人员开发的,于2013年发布的一款基于Java的微基准测试工具,现已归于JDK。官网:http://openjdk.java.net/projects/code-tools/jmh/
JMH主要用于量化Java代码的性能,主要应用场景如下:
- 对于已知的函数进行进一步优化
- 确认函数执行时间以及于参数的关系
- 比较相同功能的函数的性能
所以我们上面就是它的第三使用场景
使用 JMH 测试 replace
在 JDK9 以前需要手工引入如下两个包来引入相关的能力,目前最新的版本是 1.23
<dependency>
<groupId>org.openjdk.jmhgroupId>
<artifactId>jmh-coreartifactId>
<version>1.23version>
dependency>
<dependency>
<groupId>org.openjdk.jmhgroupId>
<artifactId>jmh-generator-annprocessartifactId>
<version>1.23version>
dependency>
然后编写测试类 StringReplaceBenchmark.java
,主要包括 2 * 3 个测试用例,分别是长字符串、短字符串配合有配字符、无匹配字符和多个匹配字符的测试。编写完成以后直接运行 main 方法即可,运行过程中会打印很多中间内容如下,为了让测试更准确,所以有一个预热环节,因为 JVM 的 JIT 机制的存在,如果某个函数被调用多次之后,JVM 会尝试将其编译成为机器码从而提高执行速度。为了让 benchmark 的结果更加接近真实情况就需要进行预热。同时增加了 7 次迭代取平均值,这样测试才更有说服力。
# Warmup Iteration 1: 300005.882 ops/s
# Warmup Iteration 2: 542183.082 ops/s
# Warmup Iteration 3: 944105.027 ops/s
# Warmup Iteration 4: 791354.184 ops/s
# Warmup Iteration 5: 692452.146 ops/s
Iteration 1: 872745.904 ops/s
Iteration 2: 1195916.316 ops/s
Iteration 3: 896864.162 ops/s
Iteration 4: 1009119.818 ops/s
Iteration 5: 1156199.641 ops/s
Iteration 6: 1150938.109 ops/s
Iteration 7: 952645.233 ops/s
下面就是全部的源码
@Fork(value = 3, jvmArgsAppend = "-Djmh.stack.lines=3")
@Warmup(iterations = 5)
@Measurement(iterations = 7)
public class StringReplaceBenchmark {
private static final String SHORT_STRING_NO_MATCH = "abc";
private static final String SHORT_STRING_ONE_MATCH = "a'bc";
private static final String SHORT_STRING_SEVERAL_MATCHES = "'a'b'c'";
private static final String LONG_STRING_NO_MATCH =
"abcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabc";
private static final String LONG_STRING_ONE_MATCH =
"abcabcabcabcabcabcabcabcabcabcabca'bcabcabcabcabcabcabcabcabcabcabcabcabc";
private static final String LONG_STRING_SEVERAL_MATCHES =
"abcabca'bcabcabcabcabcabc'abcabcabca'bcabcabcabcabcabca'bcabcabcabcabcabcabc";
@Benchmark
public void shortStringNoMatch(Blackhole blackhole) {
blackhole.consume(SHORT_STRING_NO_MATCH.replace("'", "''"));
}
@Benchmark
public void shortStringNoMatchUtils(Blackhole blackhole) {
blackhole.consume(StringUtils.replace(SHORT_STRING_NO_MATCH, "'", "''"));
}
@Benchmark
public void longStringNoMatch(Blackhole blackhole) {
blackhole.consume(LONG_STRING_NO_MATCH.replace("'", "''"));
}
@Benchmark
public void longStringNoMatchUtils(Blackhole blackhole) {
blackhole.consume(StringUtils.replace(LONG_STRING_NO_MATCH, "'", "''"));
}
@Benchmark
public void shortString1Match(Blackhole blackhole) {
blackhole.consume(SHORT_STRING_ONE_MATCH.replace("'", "''"));
}
@Benchmark
public void shortString1MatchUtils(Blackhole blackhole) {
blackhole.consume(StringUtils.replace(SHORT_STRING_ONE_MATCH, "'", "''"));
}
@Benchmark
public void longString1Match(Blackhole blackhole) {
blackhole.consume(LONG_STRING_ONE_MATCH.replace("'", "''"));
}
@Benchmark
public void longString1MatchUtils(Blackhole blackhole) {
blackhole.consume(StringUtils.replace(LONG_STRING_ONE_MATCH, "'", "''"));
}
@Benchmark
public void shortStringNMatch(Blackhole blackhole) {
blackhole.consume(SHORT_STRING_SEVERAL_MATCHES.replace("'", "''"));
}
@Benchmark
public void shortStringNMatchUtils(Blackhole blackhole) {
blackhole.consume(StringUtils.replace(SHORT_STRING_SEVERAL_MATCHES, "'", "''"));
}
@Benchmark
public void longStringNMatch(Blackhole blackhole) {
blackhole.consume(LONG_STRING_SEVERAL_MATCHES.replace("'", "''"));
}
@Benchmark
public void longStringNMatchUtils(Blackhole blackhole) {
blackhole.consume(StringUtils.replace(LONG_STRING_SEVERAL_MATCHES, "'", "''"));
}
public static void main(String[] args) throws RunnerException {
Options options = new OptionsBuilder().include(StringReplaceBenchmark.class.getSimpleName()).build();
new Runner(options).run();
}
}
说了这么多源码,里面用来很多新的技术点,下面我们就简单解释下,这样后面你就可以自己做测试了。
JMH 基本概念
@BenchmarkMode
基准测试类型:
- Throughput: 整体吞吐量,例如“1秒内可以执行多少次调用”。
- AverageTime: 调用的平均时间,例如“每次调用平均耗时xxx毫秒”。
- SampleTime: 随机取样,最后输出取样结果的分布,例如“99%的调用在xxx毫秒以内,99.99%的调用在xxx毫秒以内”
- SingleShotTime: 以上模式都是默认一次 iteration 是 1s,唯有 SingleShotTime 是只运行一次。往往同时把 warmup 次数设为0,用于测试冷启动时的性能。
- All(“all”, “All benchmark modes”);
@Warmup
一般我们前几次进行程序测试的时候都会比较慢, 所以要让程序进行几轮预热,保证测试的准确性。
- iterations就是预热轮数。
- time则是每次预热时间。
- batchSize:批处理大小,每次操作调用几次方法。
@Measurement
度量,其实就是一些基本的测试参数。
- iterations 进行测试的轮次
- time 每轮进行的时长
- timeUnit 时长单位
@Threads
每个进程中的测试线程,可用于类或者方法上。一般选择为cpu乘以2。如果配置了 Threads.MAX ,代表使用 Runtime.getRuntime().availableProcessors() 个线程。
@Fork
可用于类或者方法上。如果 fork 数是2的话,则 JMH 会 fork 出两个进程来进行测试。
@OutputTimeUnit
这个比较简单了,基准测试结果的时间类型。一般选择秒、毫秒、微秒。
@Benchmark
方法级注解,表示该方法是需要进行 benchmark 的对象,用法和 JUnit 的 @Test 类似,就是我们上面的示例中的 6 个方法。
@Param
@Param 可以用来指定某项参数的多种情况,直接把它注解到变量上,就可以做循环测试了。
@Setup
方法级注解,这个注解的作用就是我们需要在测试之前进行一些准备工作,可以类比为 @Before
@TearDown
方法级注解,这个注解的作用就是我们需要在测试之后进行一些结束工作,比如关闭线程池,数据库连接,流等。
Runner
全部准备好了以后需要运行,所以用下面的 Runner 进行运行,同时传递 Options
Options options =
new OptionsBuilder()
.include(StringReplaceBenchmark.class.getSimpleName())
.build();
new Runner(options).run();
最常用的几个方法
- include 正则包含的要测试类名,当前例子已经确定,所以就是类的 simpleName
- output 如果传入会把结果输出到文件,比如 ~/benchmark.log
- forks 重新配置 fork 的数量,会覆盖之前的配置
- warmupIterations 预热次数,会覆盖之前的配置
- measurementIterations 迭代次数,会覆盖之前的配置
- threads 线程数量,会覆盖之前的配置
源码&课后题
上文中的所有代码都存放在了下面的仓库https://github.com/juice-resume/java-programming
看了这篇文章是不是除了知道以后怎么使用 repalce
,那么是不是想动动小手试试平时用的 BeanUtils、HashMap、ArrayList 性能都是怎么样的呢?不过要记住 JMH 只适合细粒度的方法测试,并不适合复杂的系统测试。
点个赞呗