java-使用jmh基准测试框架比较五种字符串拼接性能
引言
Java中提供了5种字符串拼接的方法,使用+拼接字符串是最长见的方法。除此还有StringBuilder、StringBuffer、MessageFormat、StringFormat
单纯从拼接执行时间上比较下五种方式的性能。
比较结果
先看下执行结果,其中的Score是执行耗时(微妙),Error可以看做是误差。
执行时间由短到长为:+ < StringBuilder < StringBuilder < MessageFormat < StringFormat
Benchmark Mode Cnt Score Error Units
JmhTest.testMessageFormat avgt 5 722.346 ± 134.540 us/op
JmhTest.testStringBase avgt 5 6.905 ± 2.604 us/op
JmhTest.testStringBuffer avgt 5 8.291 ± 5.311 us/op
JmhTest.testStringBuilder avgt 5 7.192 ± 3.861 us/op
JmhTest.testStringFormat avgt 5 1273.906 ± 69.336 us/op
jmh基准测试
添加依赖
<!--基准测试-->
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.35</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.35</version>
</dependency>
JMH注解说明
@BenchMarkMode 设置基准测试的模式 【方法或者类】
@OutPutTimeUnit 报告结果的默认时间单位【类、方法】
@Warmup 预热,设置具体的配置参数如次数,时间等
@Measurement 类似预热,但是设置的是测量时的
@Fork 整体测试几次
@State 设置配置对象的作用域,定义线程之间的共享程度
@Setup 线程执行前的配置函数、初始化
@TearDown 测试后处理操作 【方法】
@BenchMark 标记测试基准 【方法】
@OperationsPerInvocation 与基准进行多操作通信,运行JMH调整
- @BenchMarkMode
设置运行基准测试的模式,可以选择放在方法上面,只对该方法生效。
Mode.Throughput : 吞吐量模式,获得单位时间的操作数量,连续运行@BenchMark的方法,计算所有的工作线程的总吞吐量。
Mode.AverageTime: 平均时间模式, 获得每次操作的平均时间,计算所有工作线程的平均时间。
Mode.SimpleTime: 时间采样模式, 对每一个操作函数的时间进行采样,连续运行@BenchMark的函数,随机抽取运行所需要的时间。
Mode.SingleShotTime: 单次触发模式, 测试单次操作的时间,连续运行@BenchMark函数,只运行一次并计算时间: 该模式只是运行一次@BenchMark函数,所以需要预热, 如果基准数值小,使用SimpleTime模式采样。
Mode.All : 无模式,采用所有的基准模式,效果最好。
编写测试代码
运行main方法启动测试,耐性等待测试完成即可。
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.text.MessageFormat;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.AverageTime) //基准测试的默认模式
@OutputTimeUnit(TimeUnit.MICROSECONDS) //时间单位:纳秒、微妙、毫秒、秒、分、时
@State(Scope.Thread)
@Fork(1) //进程数一般设置为1
//@Threads(1) //线程数
@Warmup(iterations = 2,time = 2) //预热迭代次数,time控制每次迭代的间隔时间(默认秒)
@Measurement(iterations = 5,time = 2) //测量迭代次数,time控制每次迭代的间隔时间(默认秒)
public class JmhTest {
private int _loop = 1000;
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder().include(JmhTest.class.getSimpleName()).build();
new Runner(opt).run();
}
//StringBuffer
@Benchmark //被测试的方法
public void testStringBuffer() {
for(int i = 0; i< _loop; i++){
StringBuffer sbr = new StringBuffer();
String s = sbr.append("1111").append(",")
.append("2222").append(",")
.append("3333").append(",")
.append("4444").toString();
}
}
//StringBuilder
@Benchmark //被测试的方法
public void testStringBuilder() {
for(int i = 0; i< _loop; i++){
StringBuilder sbr = new StringBuilder();
String s = sbr.append("1111").append(",")
.append("2222").append(",")
.append("3333").append(",")
.append("4444").toString();
}
}
//MessageFormat
@Benchmark
public void testMessageFormat() {
for(int i = 0; i< _loop; i++){
String s = MessageFormat.format("{0},{1},{2},{3}","1111","2222","3333","4444");
}
}
//String.format
@Benchmark
public void testStringFormat() {
for(int i = 0; i< _loop; i++){
String s = String.format("%s,%s,%s,%s","1111","2222","3333","4444");
}
}
//字符串拼接
@Benchmark
public void testStringBase() {
for(int i = 0; i< _loop; i++){
String s = "1111";
s+=",2222";
s+=",3333";
s+=",4444";
}
}
}
测试结果说明
以上真对5个函数进行基准测试,测试的配置是每个函数预热执行1次,对之后的5次进行平均执行时间统计。
真对使用Mode.AverageTime模式测试,每个函数执行完成后的输出为:
Result "com.cnpc.epai.researchdata.data.service.JmhTest.testStringFormat":
1254.609 ±(99.9%) 174.543 us/op [Average]
(min, avg, max) = (1174.821, 1254.609, 1282.413), stdev = 45.328
CI (99.9%): [1080.066, 1429.151] (assumes normal distribution)
最终的执行结果是:
Benchmark Mode Cnt Score Error Units
JmhTest.testMessageFormat avgt 5 722.346 ± 134.540 us/op
JmhTest.testStringBase avgt 5 6.905 ± 2.604 us/op
JmhTest.testStringBuffer avgt 5 8.291 ± 5.311 us/op
JmhTest.testStringBuilder avgt 5 7.192 ± 3.861 us/op
JmhTest.testStringFormat avgt 5 1273.906 ± 69.336 us/op
其中Score为执行耗时(微妙)Error为误差;
结论:
1.使用StringBuilder的方式是效率最高的。
2.如果不是在循环体中进行字符串拼接的话,直接使用+就好了。
3.如果在并发场景中进行字符串拼接的话,要使用StringBuffer来代替StringBuilder。