JMH-分析equals的优化空间

  • 使用了 JMH之后,对于每一个小方法,都可以做一个非常深入的研究,对比,不一样的写法,不一样的风格,到底有没有区别,到底谁最好。

本次深入研究一下 equals

  • 代码来源是 查看 阿里开源包中的工具包中的代码,看到之后,眼前一亮,然后进入了深思。

     /**
         * check whether two components are equal
    * * @param src source component * @param target target component * @param component type * @return
    * (null, null) == true * (1L,2L) == false * (1L,1L) == true * ("abc",null) == false * (null,"abc") == false */ public static boolean isEquals(E src, E target) { return null == src && null == target || null != src && null != target && src.equals(target); }
  • 对于 equals的使用,常规会有两种方式:

    • 1) 常量.equals(obj)

      这判断,没有问题,有些经验的程序员,都知道把常量放在前面,避免空指针

      1. obj1.equals(obj2)

      这样使用,会有 Java.lang.NullPointerException 的风险 ,所以常规,会有一个 if(null != str1){}作为前提。

  • 阿里将其包装了一下:

      1. 好处1:写成一个工具类,直接使用即可,避免新人犯错误的成本。
      1. 好处2:这个工具类,包裹住 非空的判断, 让使用的代码,简洁。
    • 3)疑问:这样包装,是否会有性能的提升空间?

下面开始进行性能的详细分析

1)以往常规的测试方法,会写一个 main方法,进行测试:
  public static void main(String[] args) {
        int[] count = {10000,100000,1000000,10000000};
        for(int num : count){
            System.out.println("------------执行"+num+"次------------");
            testEqu(num);
        }
    }
    public static void testEqu(int count){
        Affect affect = new Affect();
        String a = "1";
        String b = "2";
        int num = count;
        int temp = 0;
        while (num-- > 0) {
            if (a != null && a.equals(b)) {
                temp++;
            }
            temp++;
        }
        System.out.println("  a.equals(b):"+affect.cost());
        affect = new Affect();
        num = count;
        temp = 0;
        while (num-- > 0) {
            if (isEquals(a, b)) {
                temp++;
            }
            temp++;
        }
        System.out.println("isEquals(a, b):"+affect.cost());
    }

运行结果:

------------执行10000次------------
  a.equals(b):1
isEquals(a, b):0
------------执行100000次------------
  a.equals(b):5
isEquals(a, b):2
------------执行1000000次------------
  a.equals(b):3
isEquals(a, b):18
------------执行10000000次------------
  a.equals(b):29
isEquals(a, b):28

看到这个结果,小伙子们,是不是会觉得非常奇怪,为什么呢?

看到这个结果,小伙子们,是不是会觉得非常奇怪,为什么呢?

看到这个结果,小伙子们,是不是会觉得非常奇怪,为什么呢?

  • 然后我们来改造一下代码:
public static void main(String[] args) throws InterruptedException {
        int[] count = {10000,100000,1000000,10000000};
        for(int num : count){
            System.out.println("------------执行"+num+"次------------");
            testEqu(num);
            Thread.sleep(2000); // -------------------------加了这里
        }
    }
    public static void testEqu(int count) throws InterruptedException {
        Affect affect = new Affect();
        String a = "1";
        String b = "2";
        int num = count;
        int temp = 0;
        while (num-- > 0) {
            if (a != null && a.equals(b)) {
                temp++;
            }
            temp++;
        }
        System.out.println("  a.equals(b):"+affect.cost());
        Thread.sleep(2000);// -------------------------加了这里
        affect = new Affect();
        num = count;
        temp = 0;
        while (num-- > 0) {
            if (isEquals(a, b)) {
                temp++;
            }
            temp++;
        }
        System.out.println("isEquals(a, b):"+affect.cost());
    }

结果是:

------------执行10000次------------
  a.equals(b):0
isEquals(a, b):0
------------执行100000次------------
  a.equals(b):7
isEquals(a, b):2
------------执行1000000次------------
  a.equals(b):17
isEquals(a, b):3
------------执行10000000次------------
  a.equals(b):45
isEquals(a, b):34

这样看,就比较明白了,但是这是为什么呢?

  • 最主要的原因是GC的垃圾回收导致,这样写测试代码,是不准确的,变量都在一个类里,当对象的生命周期结束了,GC的过程,会影响其他代码的运作。
  • 还有一个问题,就是 JIT , 运行次数没有到一定的程度,无法进入 JIT,但是静态方法块,先天就有优势,提前进入了JIT,所以也有可能不准确

so ,引出了 JMH , 见下面

2)使用 JMH 看一下情况

代码如下:

package org.openjdk.jmh.samples;
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 static org.openjdk.jmh.samples.ArthasCheckUtils.isEquals;

@State(Scope.Thread) // 每个测试线程一个实例
public class JMHDemo01 {
    @Benchmark
    public String stringConcat() {
        String a = "1";
        String b = "2";
        int f = 0;
        if (a != null) {
            if (a.equals(b)) {
                f++;
            }
        }
        return "";
    }
    @Benchmark
    public String stringConcatIsEquals() {
        String a = "1";
        String b = "2";
        int f = 0;
        if(isEquals(a, b)) {
            f++;
        }
        return "";
    }
}

测试 main 方法

public static void main(String[] args) throws RunnerException {
    Options opt = new OptionsBuilder()
            .include(JMHDemo01.class.getSimpleName())
            .forks(1)
            .build();

    new Runner(opt).run();
}

看看结果:

Benchmark                        Mode  Cnt          Score        Error  Units
JMHDemo01.stringConcat          thrpt    5  132834643.560 ± 443289.509  ops/s
JMHDemo01.stringConcatIsEquals  thrpt    5  132995301.389 ±  52712.796  ops/s

  • 结论:

    • 使用了JMH 之后, 发现 结果非常的接近,包装了 equals 之后,性能还是提高了满多的,十几万次ops, 但是没有之前的测试的这么大的差距,只相差 0.12%.

    • 确定了 之前 考虑的 JIT的问题,他们其实是一样的方法,一样都到热区后,理论上的性能,应该是一致的。

    • 但是对于这样优化的必要性而言,还是非常有必要的,其他的优势依旧存在:

      • 1)代码整洁美观的提升
      • 2)编码质量的提升
      • 3)提前进入热区
      • 4)可以从结果中看出,优化后的方法,更加稳定

测试代码,有写的不对的地方,请指出,转载,请标明出处。

有问题,可以给我留言。

你可能感兴趣的:(JMH-分析equals的优化空间)