高性能问题
代码单元的性能测试、代码优化
按照软件工程的思想,要想完成一个项目,只要做好准备工作,编码的工作量是很小的,比如搭建一个复杂的web项目,直接分模块,比如Cfeng的预上线网站cfeng.net目前提供的几个服务就是不同的几个模块,借助JPA、Spring Boot就可以迅速完成项目的构建,个人项目麻烦的反而是前端的页面
在初期阶段,we可能为了完成任务,比如完成一个身份鉴定,可以使用多层if, 同时也可以选择使用switch,但是二者的性能和速度you 真的考虑过吗?
这里提几个ms的问题: 简单的代码的性能问题
if 和 switch 谁的性能更好?
FastJSON和 GSON谁性能更好?
程序使用Spring 框架 和不使用Spring 框架 哪个性能更好? 【虽然现在几乎不可能绕开Spring了,servlet,手动new】
HashMap 初始化的时候需要指定 初始的空间大小吗? 【安全不是这里讨论的】
HashMap 获取容器大小 需要更多的延时吗?
JDK的 炫技的Lambda表达式 是否需要消耗更多的性能?
MyBatis-plus 和 Spring-Data-JPA 哪个框架的性能更好?
Spring-Data-Redis和分布式的Rdission哪个效率高?
… 就各种比较性能【MinIO和FastDFS】… 怎么回答? 回答的思路?
性能测试代码全部放在GITEE的cfeng-test-demo
JMH java microbenchmark harness java单元性能测试
在编写项目的时候,作为决策者选择框架的时候就难免需要考虑性能问题,多个框架都可以解决问题,这里就面临选择,而编代码过程更会遇到更细节的问题,也会涉及到性能的比较 : 比如 基于STOMP的聊天服务,存储当前用户endpoint,是使用Set还是Map? 如何Choose? Set中使用泛型是Set《JSONObject》和Set《JavaBean》数据大小同时谁更快?
分别对两个单元使用JMH进行性能测试之后,得到的结果包含每s或者每ns的最大执行数, 得出谁的性能更好
JMH是OpenJDK开发的基准测试工具,【之前的Junit是测试结果的】,一般用于代码的性能调优,精度达到ns,适用于Java和其余基于JVM的language,是JDK9之后自带的(貌似还是需要),和web性能测试JMeter不同,JMH测试对象可以为任何方法,不仅限于REST API
<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>
JMH只要基于方法层面进行性能测试,类似与Junit单元测试,测试的目的不同,为代码优化过程;
JMH的使用场景:
- 准确知道某方法执行的时间,执行时间和输入之间的相关性
- 对比接口的不同实现在给定条件下的吞吐量【性能】
- 查看多少百分比的请求在多少时间内完成
JMH使用
首先贴一段实践的代码
import org.openjdk.jmh.annotations.Benchmark;
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;
/**
* @author Cfeng
* @date 2022/9/1
*/
public class JMHExampleTest {
/**
* 这里基准函数
*/
@Benchmark
public void jmhMethod() {
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(JMHExampleTest.class.getSimpleName()) //基准类
.forks(1)
.build();
new Runner(opt).run();
}
}
JMH编译期间生成基准代码,在基准列表中将待测性能的方法注册为基准数值,该待测方法应该是公开的且可以抛出异常; 抛出异常之后JMH的Runner就会执行错误,直接结束,进入下一个基准测试; 衡量的基准,将空函数作为基准进行参考【空函数没有内容,执行速度最快】
JMH 的运行依赖于org.openjdk.jmh.runner.options.Options的配置, 配置Options之后,就可以通过org.openjdk.jmh.runner.Runner进行运行
- include: 运行时包含的基准类
- forks: 指定方法重复执行的次数
执行后可以看到大量的迭代,大量的吞吐量,每隔函数的开销
# Warmup: 5 iterations, 10 s each
# Measurement: 5 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: com.Jning.cfengtestdemo.jmhTest.JMHExampleTest.jmhMethod
# Run progress: 0.00% complete, ETA 00:01:40
# Fork: 1 of 1
# Warmup Iteration 1: 2432867074.037 ops/s
# Warmup Iteration 2: 2560473580.920 ops/s
# Warmup Iteration 3: 2602717289.592 ops/s
# Warmup Iteration 4: 2585859810.812 ops/s
# Warmup Iteration 5: 2588578460.594 ops/s
Iteration 1: 2606056999.216 ops/s
Iteration 2: 2469027629.800 ops/s
Iteration 3: 2595788786.098 ops/s
Iteration 4: 2591829783.488 ops/s
Iteration 5: 2600675478.647 ops/s
Result "com.Jning.cfengtestdemo.jmhTest.JMHExampleTest.jmhMethod":
2572675735.450 ±(99.9%) 224052512.720 ops/s [Average]
(min, avg, max) = (2469027629.800, 2572675735.450, 2606056999.216), stdev = 58185726.044
CI (99.9%): [2348623222.730, 2796728248.170] (assumes normal distribution)
Benchmark Mode Cnt Score Error Units
Jning.cfengtestdemo.jmhTest.JMHExampleTest.jmhMethod thrpt 5 2572675735.450 ± 224052512.720 ops/s
ops 为Operations Per Second 每秒的操作数; 可以看到空函数的速度非常快, 可以看到空函数的执行为每s 执行25亿次
@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 标记测试基准 【方法】
放在方法上面表明测试的基准
@OperationsPerInvocation 与基准进行多操作通信,运行JMH调整
这里就可以测试循环内部的代码
@BenchMark
@OperationsPerInvocation(10)
public void test() {
for(int i = 0; i < 10; i ++) {
//xxx
}
}
SpringBoot中使用JMH
SpringBoot中使用的区别就是需要获取容器中的Bean,进行测试, 在测试中,使用@Setup进行初始化, 使用SpringApplication.run(XXXX) 获取容器,获取测试所需要的Bean
package com.Jning.cfengtestdemo.jmhTest;
import com.Jning.cfengtestdemo.DemoApplication;
import com.Jning.cfengtestdemo.controller.TestUserController;
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 org.springframework.boot.SpringApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* @author Cfeng
* @date 2022/9/1
*/
@BenchmarkMode(Mode.AverageTime) //平均时间模式
@State(Scope.Benchmark) //使用的SpringBoot容器,都是无状态单例Bean,无安全问题,可以直接使用基准作用域BenchMark
@OutputTimeUnit(TimeUnit.NANOSECONDS) //这里是ns为单位
@Fork(1) //整体平均执行1次
@Warmup(iterations = 5,time = 1,timeUnit = TimeUnit.SECONDS) //预热1s
@Measurement(iterations = 5,time = 1,timeUnit = TimeUnit.SECONDS) //测试也是1s、五遍
public class JMHExampleTest {
//springBoot容器
private ApplicationContext context;
//待测试的TestUserController
private TestUserController userController;
/**
* 初始化,获取springBoot容器,run即可,同时得到相关的测试对象
*/
@Setup
public void init() {
//容器获取
context = SpringApplication.run(DemoApplication.class);
//获取对象
userController = context.getBean(TestUserController.class);
}
@Benchmark
public void getUserList() {
List userList = userController.queryAll();
}
@Benchmark
public void testIncrement() {
context.getBeanDefinitionCount();
}
/**
* 测试的后处理操作,关闭容器,资源清理
*/
@TearDown
public void down() {
// System.out.println("结束测试,后处理操作");
//使用子类ConfigurableApplicationContext关闭
((ConfigurableApplicationContext)context).close();
}
public static void main(String[] args) throws RunnerException {
// Options opt = new OptionsBuilder()
// .include(JMHExampleTest.class.getSimpleName()) //基准类
// .forks(1) //重复执行的次数
// .build();
//使用注解之后只需要配置一下include即可,fork和warmup、measurement都是注解
Options opt = new OptionsBuilder()
.include(".*" + JMHExampleTest.class.getSimpleName() + ".*")
.build();
new Runner(opt).run();
}
}
多使用注解代替在Options中进行配置
Benchmark Mode Cnt Score Error Units
Jning.cfengtestdemo.jmhTest.JMHExampleTest.getUserList avgt 5 621247.176 ± 109697.833 ns/op
Jning.cfengtestdemo.jmhTest.JMHExampleTest.testIncrement avgt 5 1.860 ± 0.237 ns/op
这里可以看到测试两个方法,第一个是controller接口的获取所有的用户, 第二个为获取容器bean的数量,第一个每次操作需要621247ns 相比getCount来说消耗庞大, 这是因为方法1需要进行数据库查询,需要进行数据库连接,2不需要
同理可以使用StringBuilder和StringBuffer,StringBuilder运行的速度更快【Buffer更安全】
其他的性能问题也可以进行比较得出,比如
Lambda表达式,一般情况下增强for循环比Stream更快, 但是预热后Lambda、stream可能更快
JMH建议在打包之后再使用,利用jar进行命令行方式的启动测试,因为IDEA本身也会消耗资源
界面化和日志输出都很消耗性能, 比如空函数和System.out.print(XXX)就差距巨大,在高并发的操作时就避免输出日志