try catch 对性能影响

引言

之前一直没有去研究try catch的内部机制,只是一直停留在了感觉上,正好这周五开会交流学习的时候,有人提出了相关的问题。借着周末,正好研究一番。

讨论的问题

当时讨论的是这样的问题:
比较下面两种try catch写法,哪一种性能更好。

        for (int i = 0; i < 1000000; i++) { try { Math.sin(j); } catch (Exception e) { e.printStackTrace(); }
        }
        try {
            for (int i = 0; i < 1000000; i++) {
                Math.sin(j);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

结论

在没有发生异常时,两者性能上没有差异。如果发生异常,两者的处理逻辑不一样,已经不具有比较的意义了。

分析

要知道这两者的区别,最好的办法就是查看编译后生成的Java字节码。看一下try catch到底做了什么。
下面是我的测试代码

package com.kevin.java.performancetTest;

import org.openjdk.jmh.annotations.Benchmark;

/** * Created by kevin on 16-7-10. */
public class ForTryAndTryFor {

    public static void main(String[] args) {
        tryFor();
        forTry();
    }

    public static void tryFor() {
        int j = 3;
        try {
            for (int i = 0; i < 1000; i++) {
                Math.sin(j);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void forTry() {
        int j = 3;
        for (int i = 0; i < 1000; i++) {
            try {
                Math.sin(j);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

使用javap -c fileName.class输出对应的字节码

$ javap -c ForTryAndTryFor.class
Compiled from "ForTryAndTryFor.java"
public class com.kevin.java.performancetTest.ForTryAndTryFor {
  public com.kevin.java.performancetTest.ForTryAndTryFor();
    Code:
       0: aload_0
       1: invokespecial #1 // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: invokestatic  #2 // Method tryFor:()V
       3: invokestatic  #3 // Method forTry:()V
       6: return

  public static void tryFor();
    Code:
       0: iconst_3
       1: istore_0
       2: iconst_0
       3: istore_1
       4: iload_1
       5: sipush        1000
       8: if_icmpge     23
      11: iload_0
      12: i2d
      13: invokestatic  #4 // Method java/lang/Math.sin:(D)D
      16: pop2
      17: iinc          1, 1
      20: goto          4
      23: goto          31
      26: astore_1
      27: aload_1
      28: invokevirtual #6 // Method java/lang/Exception.printStackTrace:()V
      31: return
    Exception table:
       from    to  target type
           2    23    26   Class java/lang/Exception

  public static void forTry();
    Code:
       0: iconst_3
       1: istore_0
       2: iconst_0
       3: istore_1
       4: iload_1
       5: sipush        1000
       8: if_icmpge     31
      11: iload_0
      12: i2d
      13: invokestatic  #4 // Method java/lang/Math.sin:(D)D
      16: pop2
      17: goto          25
      20: astore_2
      21: aload_2
      22: invokevirtual #6 // Method java/lang/Exception.printStackTrace:()V
      25: iinc          1, 1
      28: goto          4
      31: return
    Exception table:
       from    to  target type
          11    17    20   Class java/lang/Exception
}

指令含义不是本文的重点,所以这里就不介绍具体的含义,感兴趣可以到Oracle官网查看相应指令的含义The Java Virtual Machine Instruction Set
好了让我们来关注一下try catch 到底做了什么。我们就拿forTry方法来说吧,从输出看,字节码分两部分,code(指令)和exception table(异常表)两部分。当将java源码编译成相应的字节码的时候,如果方法内有try catch异常处理,就会产生与该方法相关联的异常表,也就是Exception table:部分。异常表记录的是try 起点和终点,catch方法体所在的位置,以及声明捕获的异常种类。通过这些信息,当程序出现异常时,java虚拟机就会查找方法对应的异常表,如果发现有声明的异常与抛出的异常类型匹配就会跳转到catch处执行相应的逻辑,如果没有匹配成功,就会回到上层调用方法中继续查找,如此反复,一直到异常被处理为止,或者停止进程。具体介绍可以看这篇文章How the Java virtual machine handles exceptions
所以,try 在反映到字节码上的就是产生一张异常表,只有发生异常时才会被使用。由此得到出开始的结论。
这里再对结论扩充:
try catch与未使用try catch代码区别在于,前者禁止try语句块中的代码进行优化,例如重排序,try catch里面的代码是不会被编译器优化重排的。对于上面两个函数而言,只是异常表中try起点和终点位置不一样。至于刚刚说到的指令重排的问题,由于for循环条件部分符合happens- before原则,因此两者的for循环都不会发生重排。当然只是针对这里而言,在实际编程中,还是提倡try catch范围尽量小,这样才可以充分发挥java编译器的优化能力。

测试验证

既然通过字节码已经分析出来了,两者性能没有差异。那我们就来检测一下吧,看看到底是不是如前面分析的那样。
在正式开始测试时,首先我们要明白,一个好的测试方法,就是尽量保证我们的测试不被其他因素所歪曲污染而影响测试的结果。那应该使用什么方法来测试我们的代码呢?

不正确的测试

这里首先说一下常见的几种错误的测量方法,测量一个方法的执行时间,最容易想到的应该是下面这种了:

long startTime = System.currentTimeMillis();

doReallyLongThing();

long endTime = System.currentTimeMillis();

System.out.println("That took " + (endTime - startTime) + " milliseconds");

但是我会跟你说,这个方式不准确,我这里给大家展示一下我的使用上面的方式来进行测试的结果

package com.kevin.java.performancetTest;

/** * Created by kevin on 16-7-10. */
public class ForTryAndTryFor {

    public static void main(String[] args) {
        forTry();
        tryFor();
    }

    public static void tryFor() {

        long startTime = System.currentTimeMillis();

        int j = 3;
        try {
            for (int i = 0; i < 1000000; i++) {
                Math.sin(j);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        long endTime = System.currentTimeMillis();

        System.out.println("tryFor " + (endTime - startTime) + " milliseconds");
    }

    public static void forTry() {

        long startTime = System.currentTimeMillis();


        int j = 3;
        for (int i = 0; i < 1000000; i++) {
            try {
                Math.sin(j);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        long endTime = System.currentTimeMillis();

        System.out.println("forTry " + (endTime - startTime) + " milliseconds");

    }
}

测试结果

如果你这样就认为forTry比tryFor快,那就大错特错了。如果你再测试多几次就会发现问题了,我再列出一下结果
try catch 对性能影响_第1张图片

try catch 对性能影响_第2张图片

try catch 对性能影响_第3张图片
从结果看来,绝大多数时候,tryFor比forTry快。那是不是可以说tryFor比forTry快了呢?如果没有前面分析,你会不会就这样下定论了呢?问题就出现在这个绝大多数,当你运行的次数越多,就越发的体会到结果的不可靠。

至少有下面两点给人不可靠的感觉:

  1. 每次的执行时间都相差很大。同一个函数会出现,两次执行结果可能相差好几倍的情况。
  2. 其次,偶尔forTry会比tryFor快

那是什么导致了结果如此的不可靠?原因至少有下面这些:

  1. System.currentTimeMillis()测量的只是逝去的时间,并没有反映出cpu执行该函数真正消耗的时间。
    这导致线程未被分配cpu资源时,等待cpu的时间也会被计算进去
  2. JIT优化导致结果出现偏差。
    像这种多次循环非常容易触发JIT的优化机制,关于JIT,这里简短的介绍一下

    在Java编程语言和环境中,即时编译器(JIT compiler,just-in-time compiler)是一个把Java的字节码(包括需要被解释的指令的程序)转换成可以直接发送给处理器的指令的程序。当你写好一个Java程序后,源语言的语句将由Java编译器编译成字节码,而不是编译成与某个特定的处理器硬件平台对应的指令代码(比如,Intel的Pentium微处理器或IBM的System/390处理器)。字节码是可以发送给任何平台并且能在那个平台上运行的独立于平台的代码。

    简单来说,JIT会将某些符合条件(比如,频繁的循环)的字节码被编译成目标的机器指令直接执行,从而加快执行速度。可以通过配置-XX:+PrintCompilation参数,观察JIT。当JIT执行优化时,会在终端输出相应的优化信息。

  3. 类加载时间也被统计进来了。
    类首次被使用时,会触发类加载,产生了时间消耗。

由上面的分析不难看出为什么绝大多数时候tryFor会比forTry快了,JIT编译耗时和类加载时间会被统计到第一个执行的函数forTry里面。要验证这个也非常简单,把两个函数的调用顺序互换,然后再进行测试。
当然,还有一点不能忽略的是System.currentTimeMillis()并不是统计cpu真正执行时间,所以可能测试的结果会有出入。可以使用JProfiler配合Intellij IDEA进行测试,下面会提到。由于这些因素影响着测试结果,从而使得测试结果扑朔迷离。

那有什么办法解决上面的问题呢?下面列出我的解决办法

  1. 不要使用System.currentTimeMillis()亦或者使用System.nanoTime()(可能你会看到有些建议使用这个来测试,但是它跟System.currentTimeMillis()区别,仅仅在于时间的基准不同和精度不同,但都表示的是逝去的时间,所以对于测试执行时间上,并没有什么区别。因为都无法统计cpu真正执行时间。)我这里使用了JProfiler性能测试工具,它可以测量出cpu真正的执行时间。具体安装使用方法可以自行google百度。这不是最终使用的测试工具,所以就不做详细介绍了。
  2. 对于后两者,需要加入Warmup(预热)阶段。预热阶段就是不断运行你的测试代码,从而使得代码完成初始化工作(类加载),并足以触发JIT编译机制。一般来说,循环几万次就可以预热完毕。
    是不是这样就可以得到完美的结果了。很不幸,并没有那么简单,JIT机制并没有想象的这么简单,要做到以下这些点你才能得到比较真实的结果。下面摘录至how-do-i-write-a-correct-micro-benchmark-in-java排名第一的答案

    Tips about writing micro benchmarks from the creators of Java
    HotSpot:

    Rule 0: Read a reputable paper on JVMs and micro-benchmarking. A good one is Brian Goetz, 2005. Do not expect too much from
    micro-benchmarks; they measure only a limited range of JVM performance
    characteristics.

    Rule 1: Always include a warmup phase which runs your test kernel all the way through, enough to trigger all initializations and
    compilations before timing phase(s). (Fewer iterations is OK on the
    warmup phase. The rule of thumb is several tens of thousands of inner
    loop iterations.)

    Rule 2: Always run with -XX:+PrintCompilation, -verbose:gc, etc., so you can verify that the compiler and other parts of the JVM
    are not doing unexpected work during your timing phase.

    Rule 2.1: Print messages at the beginning and end of timing and warmup phases, so you can verify that there is no output from Rule 2
    during the timing phase.

    Rule 3: Be aware of the difference between -client and -server, and OSR and regular compilations. The -XX:+PrintCompilation flag
    reports OSR compilations with an at-sign to denote the non-initial
    entry point, for example: Trouble$1::run @ 2 (41 bytes). Prefer
    server to client, and regular to OSR, if you are after best
    performance.

    Rule 4: Be aware of initialization effects. Do not print for the first time during your timing phase, since printing loads and
    initializes classes. Do not load new classes outside of the warmup
    phase (or final reporting phase), unless you are testing class loading
    specifically (and in that case load only the test classes). Rule 2 is
    your first line of defense against such effects.

    Rule 5: Be aware of deoptimization and recompilation effects. Do not take any code path for the first time in the timing phase, because
    the compiler may junk and recompile the code, based on an earlier
    optimistic assumption that the path was not going to be used at all.
    Rule 2 is your first line of defense against such effects.

    Rule 6: Use appropriate tools to read the compiler’s mind, and expect to be surprised by the code it produces. Inspect the code
    yourself before forming theories about what makes something faster or
    slower.

    Rule 7: Reduce noise in your measurements. Run your benchmark on a quiet machine, and run it several times, discarding outliers. Use
    -Xbatch to serialize the compiler with the application, and consider
    setting -XX:CICompilerCount=1 to prevent the compiler from running
    in parallel with itself.

    Rule 8: Use a library for your benchmark as it is probably more efficient and was already debugged for this sole purpose. Such as
    JMH, Caliper or Bill and Paul’s Excellent UCSD Benchmarks
    for Java.

    还可以参考Java theory and practice: Anatomy of a flawed microbenchmark
    认真看完这些,你就会发现,要保证microbenchmark结果的可靠,真不是一般的难!!!

    那就没有简单可靠的测试方法了吗?如果你认真看完上面提到的点,你应该会注意到Rule 8,没错,我就是使用Rule8提到的JMH来。这里摘录一段网上的介绍

    JMH是新的microbenchmark(微基准测试)框架(2013年首次发布)。与其他众多框架相比它的特色优势在于,它是由Oracle实现JIT的相同人员开发的。特别是我想提一下AlekseyShipilev和他优秀的博客文章。JMH可能与最新的Oracle JRE同步,其结果可信度很高。

    JMH官方主页:http://openjdk.java.net/projects/code-tools/jmh/

正确的测试

测试环境:

JVM版本:
java version "1.8.0_91"
Java(TM) SE Runtime Environment (build 1.8.0_91-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.91-b14, mixed mode)

系统:
Linux Mint 17.3 Rosa 64bit

配置:i7-4710hq+16g

工具:Intellij IDEA 2016+JMH的jar包+JMH intellij plugin

插件具体使用可以看JMH插件Github项目地址,上面有介绍使用细节

测试代码:

package com.kevin.java.performancetTest;

import org.openjdk.jmh.annotations.Benchmark;

/** * Created by kevin on 16-7-10. */
public class ForTryAndTryFor {

    public static void main(String[] args) {
        tryFor();
        forTry();
    }

    @Benchmark
    public static void tryFor() {
        int j = 3;
        try {
            for (int i = 0; i < 1000; i++) {
                Math.sin(j);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Benchmark
    public static void forTry() {
        int j = 3;
        for (int i = 0; i < 1000; i++) {
            try {
                Math.sin(j);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

测试结果

JMH会做执行一段时间的WarmUp,之后才开始进行测试。这里只是截取结果部分,运行过程输出就不放出来了

# Run complete. Total time: 00:02:41 

Benchmark                                 Mode  Cnt   Score   Error   Units
performancetTest.ForTryAndTryFor.forTry  thrpt   40  26.122 ± 0.035  ops/ms
performancetTest.ForTryAndTryFor.tryFor  thrpt   40  25.535 ± 0.087  ops/ms
# Run complete. Total time: 00:02:41

Benchmark                                  Mode     Cnt  Score    Error  Units
performancetTest.ForTryAndTryFor.forTry  sample  514957  0.039 ±  0.001  ms/op
performancetTest.ForTryAndTryFor.tryFor  sample  521559  0.038 ±  0.001  ms/op

每个函数都测试了两编,总时长都是2分41秒
主要关注Score和Error两列,±表示偏差。
第一个结果的意思是,每毫秒调用了 26.122 ± 0.035次forTry函数,每毫秒调用了 25.535 ± 0.087次tryFor函数,第二个结果表示的是调用一次函数的时间。

从结果中,可以看到两个函数性能并没有差异,与之前的分析吻合。

参考链接

http://stackoverflow.com/questions/16451777/is-it-expensive-to-use-try-catch-blocks-even-if-an-exception-is-never-thrown
http://stackoverflow.com/questions/504103/how-do-i-write-a-correct-micro-benchmark-in-java

你可能感兴趣的:(java)