项目:性能测试框架

该项目是模拟JMH(Java Microbenchmark Harness)的基准测试框架,自己实现了一个性能测试框架,用于测试系统在特定负载的情况下,相应时间和稳定性的表现情况。

项目功能有:

  • 自动加载测试用例;
  • 通过接口标记待测试类;
  • 通过注解标记待测试方法;
  • 通过注解实现多级配置

该项目中以下列两个测试为例:

  1. 测试String类中直接使用“+”字符串相加与StringBuilder的append()方法相加的效率区别
  2. 测试自己实现的归并排序,快速排序与Arrays.sort的效率区别

结果展示:

测试1结果
测试2结果

首先,了解一下关于JMH:

一、关于JMH:

JMH(Java Microbenchmark Harness):是专门用于代码微基准测试的工具套件。Micro Benchmark:简单的来说就是基于方法层面的基准测试,精度可以达到微秒级。当你定位到热点方法,希望进一步优化方法性能的时候,就可以使用JMH对优化的结果进行量化的分析。和其他竞品相比——如果有的话,JMH最有特色的地方就是,它是由Oracle内部实现JIT的那拨人开发的。
关于JMH的更多相关,可以参考:CSDN博主「秦沙」的原创文章:JMH使用说明

二,可能影响性能的因素:

可能影响性能的因素:

在程序执行中,可能有多种因素会影响测试的结果,主要原因还是以下几点:
1.执行时间过短
2.实验次数太少
3.优化原因(AOT/JIT编译器优化)
4.预热
5.其他原因

三、项目准备:

3.1:@Retention注解:

注解@Retention可以用来修饰注解,是注解的注解,称为元注解。
Retention注解有一个属性value,是RetentionPolicy类型的,Enum RetentionPolicy是一个枚举类型,
这个枚举决定了Retention注解应该如何去保持,也可理解为Rentention 搭配 RententionPolicy使用。RetentionPolicy有3个值:CLASS RUNTIME SOURCE
按生命周期来划分可分为3类:
1、SOURCE——在源代码中出现:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;
2、CLASS——在*.class中出现:注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期;
3、RUNTIME——在类执行时出现:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;
生命周期长度 SOURCE < CLASS < RUNTIME ,所以前者能作用的地方后者一定也能作用,一般如果需要在运行时去动态获取注解信息,那只能用 RUNTIME 注解。这个项目中用到的都是:RetentionPolicy.RUNTIME

3.2:@Target注解:

注解@Target也是用来修饰注解的注解,是一个元注解。
@Target注解决定了注解的作用目标,即被描述的注解可以用在什么地方,
取值(ElementType)有:

取值描述
@Target(ElementType.TYPE)接口、类、枚举、注解
@Target(ElementType.FIELD)字段、枚举的常量
@Target(ElementType.METHOD)方法
@Target(ElementType.PARAMETER)方法参数
@Target(ElementType.CONSTRUCTOR)构造函数
@Target(ElementType.LOCAL_VARIABLE)局部变量
@Target(ElementType.PACKAGE)

3.3:定义@Measurement注解:

定义两个参数 :
group——一共进行多少组实验
iterations——一组实验调用方法多少次
可以通过改变 group和 iterations的值来改变测试的组数和每组实验调用方法的测试:
元注解@Retention与@Target的取值分别为:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD})
代码如下:

package com.test.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD})
public @interface Measurement {
    //一组实验调用方法多少次
    int iterations();
    //一共进行多少组实验
    int group();
}

3.4:定义@Benchmark注解:

标注哪些方法需要被测试,类似@Test,注解中无实际内容
元注解@Retention与@Target的取值分别为:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
代码如下:

package com.test.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;


@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Benchmark {

}

3.5:定义@WarmUp注解:

定义预热次数,默认值为0,可以在实际测试时更改
代码如下:

package com.test.annotations;

public @interface WarmUp {
    //预热,默认次数为0
    int iterations() default 0;
}

至此,annotation注解部分完成

3.6:定义Case接口:

除了Annotation外,还需要一个Case接口用来标记待测试的类,Case接口中不需要实际内容,只起一个标记作用,实现Case接口的类就是待测试的类,在通过注解从该类中寻找待测试的具体方法。
代码如下:

package com.test;
//通过接口标记待测试类
public interface Case {
}

四、待测试类加载:

步骤:

  1. 根据一个固定类,找到类加载器
  2. 根据类加载器找到类文件所在的路径
  3. 扫描路径的所有类文件
  4. 利用Case接口,找出需要的*.class文件

代码如下:


//待测试Case加载
public class CaseLoader {
    public CaseRunner load() throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException {
        String pkgDot = "com.test.cases";
        String pkg = "com/test/cases";

        List<String> classNameList = new ArrayList<String>();
        /**
         * 1.根据一个固定类,找到类加载器
         * 2.根据类加载器找到类文件所在的路径
         * 3.扫描路径的所有类文件
         * */
        ClassLoader classLoader = this.getClass().getClassLoader();
        Enumeration<URL> urls = classLoader.getResources(pkg);
        while(urls.hasMoreElements()){
            //路径
            URL url = urls.nextElement();
            if(!url.getProtocol().equals("file")){
                //如果不是*。class文件,暂时不支持
                continue;
            }
            String dirname = URLDecoder.decode(url.getPath(),"UTF-8");
            File dir = new File(dirname);
            if(!dir.isDirectory()){
                //不是目录直接返回
                continue;
            }
            //扫描该目录下的所有*.class文件,作为所有的类文件
            File[] files = dir.listFiles();
            if(files == null){
                continue;
            }
            for(File file : files){
                String filename = file.getName();
                String className = filename.substring(0,filename.length()-6);
                classNameList.add(className);
            }
        }
        List<Case> caseList = new ArrayList<Case>();
        for(String className : classNameList){
            Class<?> cls = Class.forName(pkgDot+"."+className);
            if(hasInterface(cls,Case.class)){
                caseList.add((Case)cls.newInstance());
            }
        }
        return new CaseRunner(caseList);
    }


    private boolean hasInterface(Class<?> cls,Class<?> intf){
        Class<?>[] intfs = cls.getInterfaces();
        for (Class<?> i : intfs){
            if(i == intf){
                return true;
            }

        }
        return false;
    }

}

五、获取待测试类或方法的配置:

获取一个具体待测试的方法的配置(即 group:一共进行多少组实验
iterations:一组实验调用方法多少次),有三级配置即有三次更改配置的机会:

  1. 默认配置:使用@Measurement时方法已经获取了Measurement中的默认配置
    若后续不再进行配置,则最终测试时方法的配置就是默认配置,不会导致配置为空。
           int iterations = DEFAULT_ITERATIONS;
           int group = DEFAULT_GROUP;
  1. 类级别进行配置:先获取类级别的配置,若发现类中已进行配置,且后续不再进行配置,则则最终测试时方法的配置就是类中的配置。
Measurement classMeasurement = bCase.getClass().getAnnotation(Measurement.class);
            if(classMeasurement != null){
                //若已被配置,直接赋值
                iterations = classMeasurement.iterations();
                group = classMeasurement.group();
            }
  1. 最后一次配置是方法级别的配置,在此之前需要获取找到待测试的方法,再取得每一个方法的注解,若方法中进行了配置,则使用方法定义的配置,否则继续使用原来的配置进行测试。

  2. 最终配置获得,调用对象的实例测试方法测试:

            //找到哪些方法是需要测试的方法
            Method[] methods = bCase.getClass().getMethods();
            for(Method method : methods){
                //取到每一个方法的注解,找出需要测试的方法
                Benchmark benchmark = method.getAnnotation(Benchmark.class);
                if(benchmark == null){
                    continue;
                }

                //获取需要测试方法的配置
                Measurement methodMeasurement = method.getAnnotation(Measurement.class);
                if(methodMeasurement != null){
                    iterations = methodMeasurement.iterations();
                    group = methodMeasurement.group();
                }

                //调用对象的实例测试方法测试
                runCase(bCase,method,iterations,group);

            }

至此,经过三次查看,确定了最终的配置,完整代码如下:

class CaseRunner{
    //默认配置
    private final int DEFAULT_ITERATIONS = 10;
    private final int DEFAULT_GROUP = 5;

    private final List<Case> caseList;
    public CaseRunner(List<Case> caseList) {
        this.caseList = caseList;
    }

    //TODO:没有把WarmUp预热考虑进来
    //每组实验前都应该预热
    public void run() throws InvocationTargetException, IllegalAccessException {
        for(Case bCase : caseList){
            int iterations = DEFAULT_ITERATIONS;
            int group = DEFAULT_GROUP;

            //先获取类级别的配置
            Measurement classMeasurement = bCase.getClass().getAnnotation(Measurement.class);
            if(classMeasurement != null){
                //若已被配置,直接赋值
                iterations = classMeasurement.iterations();
                group = classMeasurement.group();
            }
            //找到哪些方法是需要测试的方法
            Method[] methods = bCase.getClass().getMethods();
            for(Method method : methods){
                //取到每一个方法的注解,找出需要测试的方法
                Benchmark benchmark = method.getAnnotation(Benchmark.class);
                if(benchmark == null){
                    continue;
                }

                //获取需要测试方法的配置
                Measurement methodMeasurement = method.getAnnotation(Measurement.class);
                if(methodMeasurement != null){
                    iterations = methodMeasurement.iterations();
                    group = methodMeasurement.group();
                }

                //调用对象的实例测试方法测试方法
                runCase(bCase,method,iterations,group);

            }
        }

    }

	//此处插入视力测试方法测试方法runCase();
}

六、实例测试方法 测试

步骤:

  1. 输出实例测试方法名;
  2. 依次进入每组测试;
  3. 测试前记录当前时间,测试后再次记录,相减即是测试相应多组分别所用的时间;
  4. 将测试所用时间输出;
    代码如下:
    实例测试方法测试
    private void runCase(Case bCase,Method method,int iterations,int group) throws InvocationTargetException, IllegalAccessException {
        System.out.println(method.getName());
        for (int i = 0; i < group; i++) {
            System.out.printf("第%d次实验:",i);
            long t1 = System.nanoTime();
            for (int j = 0; j < iterations; j++) {
                //通过 Method.invoke()调用被注解的方法,即bCase
                method.invoke(bCase);
            }
            long t2 = System.nanoTime();
            System.out.printf("耗时:%d 纳秒%n",t2-t1);
        }
    }

七、实例测试方法

该项目以以下点作为测试用例:

  1. 测试String类中直接使用“+”字符串相加与StringBuilder的append()方法的效率区别
  2. 测试自己实现的归并排序,快速排序与Arrays.sort的效率区别
    如果需要测试其他方法,只需要在Case同级目录下定义一个实现Case接口的类,类中需要测试的方法再以注解即可。

7.1 归并排序,快速排序与Arrays.sort的效率区别:

方便起见,测试代码写在了一起,代码为:

package com.test.cases;

import com.test.Case;
import com.test.annotations.Benchmark;
import com.test.annotations.Measurement;

import java.util.Arrays;
import java.util.Random;

/**
 * 测试自己实现的归并排序,快速排序与Arrays.sort的耗时对比
 *
 * */

@Measurement(iterations = 10,group = 3)
public class SortCase implements Case {
    public void quickSort(int[] a){
        //1.确定基准值
        quickSortInternal(a,0,a.length-1);

    }
    //待排序区间是[low,high]
    private void quickSortInternal(int[] a,int low,int high){
        if(high <= low){
            return;
        }
        //得到基准值最终所在的下标
        int[] pivotIndexs = parition(a,low,high);

        quickSortInternal(a,low,pivotIndexs[0]);
        quickSortInternal(a,pivotIndexs[1],high);
    }

    private void swap (int[] a,int i,int j){
        int t = a[i];
        a[i] = a[j];
        a[j] = t;
    }

    private int[] parition(int[] a,int low,int high){
        int pivot = a[high];
        int less = low;
        int i = low;
        int more = high;
        while(i < more){
            if(a[i] == pivot){
                i++;
            }else if(a[i] < pivot){
                swap(a,i,less);
                i++;
                less++;
            }else{
                while(more > i && a[more] > pivot){
                    more--;
                }
                swap(a,i,more);
            }
        }
        return new int[]{less-1,more};
    }


    public void mergeSort(int[] a){

        mergeSortInternal(a,0,a.length);

    }
    private void mergeSortInternal(int[] a,int low,int high){
        if(high <= low+1){
            return;
        }
        int mid = (low+high) >> 1;
        mergeSortInternal(a,low,mid);
        mergeSortInternal(a,mid,high);
        merge(a,low,mid,high);

    }

    private void merge(int[] a,int low,int mid,int high){
        int length = high-low;
        int[] extra = new int[length];
        int i = low;
        int j = mid;
        int k =  0;
        while(i<mid && j<high){
            if(a[i]<=a[j]){
                extra[k++] = a[i++];
            }else{
                extra[k++] = a[j++];
            }
        }

        while(i < mid){
            extra[k++] = a[i++];
        }

        while(j < high){
            extra[k++] = a[j++];
        }
        System.arraycopy(extra,0,a,low,length);
    }
    //测试自己写的快速排序
    @Benchmark
    public void testQuickSort(){
        int[] a = new int[10000];
        Random random = new Random(201910713);
        for(int i = 0;i < a.length;i++){
            a[i] = random.nextInt(100000);
        }
        quickSort(a);
    }

    //测试自己写的归并排序
    @Benchmark
    public void testMergeSort(){
        int[] a = new int[10000];
        Random random = new Random(201910713);
        for(int i = 0;i < a.length;i++){
            a[i] = random.nextInt(100000);
        }
        mergeSort(a);
    }
    //测试Arrays.sort
    @Benchmark
    public void testArraysSort(){
        int[] a = new int[10000];
        Random random = new Random(201910713);
        for(int i = 0;i < a.length;i++){
            a[i] = random.nextInt(100000);
        }
        Arrays.sort(a);
    }



}

7.2 String的"+"操作与StringBuilder的append()方法对比:

分别实例化一个String类和一个StringBuilder类的对象,再给他们分别执行“+”操作和“appnd()”操作即可。
代码如下:

package com.test.cases;


import com.test.Case;
import com.test.annotations.Benchmark;
import com.test.annotations.Measurement;

/**
 *
 *  String的"+"操作与StringBuilder的append()方法对比
 * */
@Measurement(iterations = 10,group = 3)
public class AddCast implements Case {
    private static String StringAdd(){
        String s = "";
        for (int i = 0; i < 10; i++) {
            s+=i;
        }
        return s;
    }
    private static String StringBuilderAdd(){
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 10; i++) {
            sb.append(i);
        }
        return sb.toString();
    }

    @Benchmark
    public void testStringAdd(){
        StringAdd();

    }


    @Benchmark
    public void testStringBuilderAdd(){
        StringBuilderAdd();
    }



}

八、结果展示:

先看一看标准JMH的显示样式:
标准JMH的显示样式
测试1:测试String类中直接使用“+”字符串相加与StringBuilder的append()方法相加的效率区别
显示结果为:
测试1结果
测试2:测试自己实现的归并排序,快速排序与Arrays.sort的效率区别
显示结果为:
测试2结果
这个自制性能测试框架基本要求已经完成,后续有机会的话会继续改进。

九、参考资料:

JMH使用说明
JMH: 最装逼,最牛逼的基准测试工具套件
注解@Retention的作用
彻底搞懂Class.getResource和ClassLoader.getResource的区别和底层原理
Java反射机制及Method.invoke详解

十、项目源码:

项目的代码我也放在了我的GitHub:
GitHub-基准测试框架

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值