什么? 性能差异 你是百度听说的?

193 阅读6分钟

今天突然想起,去年面试的时候 还真被问过一个问题,String StringBuilder 拼接哪个更快

反正我当时是没答出来,说实话 如果不是这个面试,我估计一直关注不到

这个比较不是重点,对于性能比较 使用这个工具

一般需要性能比较 等知识点的时候,我们首先都是百度一下,然后可能会立刻知道结果,但是 不能人云亦云,人家说什么信什么,我们也需要自己 能测试 ,能拿出证据,让别人信服

这里就要说下 JMH 性能测试框架,下面是一个简单例子,只是为了演示所用

JMH 整合

pom

<dependency>
	<groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-core</artifactId>
    <version>1.23</version>
</dependency>

<dependency>
	<groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-generator-annprocess</artifactId>
    <version>1.23</version>
</dependency>

关闭 JIT

防止 jit 导致的 优化 影响性能测试结果

image.png

测试 String StringBuilder 拼接哪个更快

内存都知道,StringBuilder 内存占用少,那哪个快呢?

测试代码

@Benchmark public void testStringBuffer() { StringBuffer sb = new StringBuffer(); sb.append("xxxxxa"); sb.append("-"); sb.append("qweqw123123"); }

@Benchmark public void testStringBuilder() { StringBuilder sb = new StringBuilder(); sb.append("xxxxxa"); sb.append("-"); sb.append("qweqw123123"); }

@Benchmark public void testStringAdd() { String s1 = "xxxxxa"; String s2 = "-"; String s3 = "qweqw123123"; String s4 = s1 + s2 + s3; }

image.png

我靠,这按照道理 不是 Stringbuilder 更快么? 反正我当时 八股文是这么背的

我本以为应该是 StringBuilder 最快呢,这怎么 String 拼接最快。。。。

Stringbuild

image.png

String image.png

String的是不能变化的final,他的拼接是 生成新的 char[] 数组,对内存占用可能比较多

Stringbuilder 的append 是 在同一个char[]的基础上 拼接

image.png


从这想

那可能 拼接的String 越多,StringBuild 才能越体现出来优势,改下代码

循环次数 改为100w次,再次测试

Benchmark                          Mode  Cnt    Score     Error  Units
ABenchmarkTest.testStringAdd      thrpt    3  150.645 ��  24.663  ops/s
ABenchmarkTest.testStringBuffer   thrpt    3  109.808 ��  90.552  ops/s
ABenchmarkTest.testStringBuilder  thrpt    3  108.064 �� 100.271  ops/s

这还是 String 拼接快啊,当然了这里没说内存的事,把次数 放到 1000w次,再看看

Benchmark                          Mode  Cnt   Score   Error  Units
ABenchmarkTest.testStringAdd      thrpt    3  14.422 �� 8.952  ops/s
ABenchmarkTest.testStringBuffer   thrpt    3  11.555 �� 3.352  ops/s
ABenchmarkTest.testStringBuilder  thrpt    3  11.694 �� 2.872  ops/s

是近了,但是没查什么,果然 它们的差距是在 内存的消耗上,StringBuilder 毕竟不用创建多个 char[] 数组,人家就是在一个变量上 添加,确实省内存

不过拼接的少的 还是 直接用String 比较靠谱,但是String 会生成多个char[] 数组,导致内存浪费

总结:

  1. 如果 拼接的少 直接用String,反正人家快,但是如果项目访问量高,char[] 数组的内存浪费比较多
  2. 拼接多 / 或者你做的系统 访问量比较高 就用StringBuilder

有的时候 看到 人家的代码

if(log.isDebugEnabled()){
    log.debug("req:" + "xxx");//虽然现在大部分都是 占位符了 哈哈
}

JMH 使用

@BenchMarkMode 设置基准测试的模式 【方法或者类】

设置运行基准测试的模式,可以选择放在方法上面,只对该方法生效,在BenchMarkMode内部设置Mode的模式

Mode.Throughput : 吞吐量模式,获得单位时间的操作数量,连续运行@BenchMark的方法,计算所有的工作线程的总吞吐量 Mode.AverageTime: 平均时间模式, 获得每次操作的平均时间,计算所有工作线程的平均时间 Mode.SimpleTime: 时间采样模式, 对每一个操作函数的时间进行采样,连续运行@BenchMark的函数,随机抽取运行所需要的时间 Mode.SingleShotTime: 单次触发模式, 测试单次操作的时间,连续运行@BenchMark函数,只运行一次并计算时间: 该模式只是运行一次@BenchMark函数,所以需要预热, 如果基准数值小,使用SimpleTime模式采样 Mode.All : 无模式,采用所有的基准模式,效果最好

@OutPutTimeUnit 报告结果的默认时间单位【类、方法】

可以放在类或者方法上面,设置测试的结果的显示的时间的单位,可以是哦那个java.util.current.TimeUnit进行设置

@OutPutTimeUnit(TimeUnit.MILLSECOUNDS)

@Warmup 预热,设置具体的配置参数如次数,时间等

JVM进程启动时,类加载器将所需要的所有类加载入内存,Bootstrap Class 核心类库,比如JRE、lib等; Extension Class 由相关的ExtClassLoader加载, Application Class 由AppClassLoader负责加载

类加载过程完毕后,所有类会进入JVM cache中,但是其他与JVM启动无关类没有加载、懒加载,当应用的第一个请求到来(比如controller的一个处理器),会触发相关类第一次加载,这个过程比较耗时, 对于低延迟应用必须要避免

采用特定的策略处理加载逻辑,保证第一次请求的快速响应,称为JVM预热

设置具体的预热的参数

iterations: 预热的迭代次数 Time: 预热的时间 timeUnit: 预热的时间单位 batchSize: 每个操作的基准方法的调用次数 (batch 一批) @Measurement 类似预热,但是设置的是测量时的 测量的参数和上面的预热的参数相同

@Fork 整体测试几次

就像之前的在main函数中设置Options中设置fork的参数,之前通过new Runner 配置参数进行run

@State 设置配置对象的作用域,定义线程之间的共享程度

可以设置测试状态对象的多线程的共享程度

Scope.Benchmark: 基准状态范围, 基准作用域: 相同类型的所有实例在所有工作线程之间共享; ---- Spring多为无状态单例Bean,可以直接所有的线程共享 此状态上面的对象的SetUp方法和TearDown方法都是一个工作线程执行,每个级别一次,没有其他线程可以操作状态对象 Scope.Group: 组状态范围、组作用域: 相同类型的所有实例在同一组中的所有的线程之间共享,每一个线程组都将提供自己的状态对象 【组内共享,组间隔离】 该状态对象上面的SetUp方法和TearDown由一个组线程执行 Scope.Thread: 线程状态范围,线程作用域: 相同类型的所有实例都不同(不是单例的),在同一个基准中注入了多个状态对象,此状态的SetUp和TearDown方法由单个工作线程独占执行 @Setup 线程执行前的配置函数、初始化 该注解只能在配置函数上面,Setup方法只由一个可以访问State对象的线程执行,一般就是一个特定的工作线程执行,如果状态共享,那么就可能由不同的线程执行(Thread 作用域)

@TearDown 测试后处理操作 【方法】

放置在方法上面进行测试后处理操作,一般测试后清理资源

@BenchMark 标记测试基准 【方法】

放在方法上面表明测试的基准

总结

这是测试性能的,你可能想问 内存怎么看,IDEA 有一个 profile 插件 可能看内存的区别

有的时候 你想看堆中有什么,栈还存在不,不想根据理论推,想实际看看,阿里 arthas 和 jstat 等jvm命令 可以满足

这里其实就有了 性能 和 内存的测试办法,之后就用这个当作证据

但是也不是万能的 ,比如 JIT优化,或者 走了缓存等等 可能会导致你的测试结果不对,也需要注意,没有银弹