前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >try catch 对性能影响

try catch 对性能影响

作者头像
全栈程序员站长
发布2022-07-04 09:03:32
1.5K0
发布2022-07-04 09:03:32
举报

大家好,又见面了,我是你们的朋友全栈君。

引言

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

讨论的问题

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

代码语言:javascript
复制
        for (int i = 0; i < 1000000; i++) { try { Math.sin(j); } catch (Exception e) { e.printStackTrace(); }
        }
代码语言:javascript
复制
        try {
            for (int i = 0; i < 1000000; i++) {
                Math.sin(j);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

结论

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


分析

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

代码语言:javascript
复制
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输出对应的字节码

代码语言:javascript
复制
$ 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官网查看相应指令的含义

好了让我们来关注一下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代码区别在于,前者阻止Java对try块的代码的一些优化,例如重排序。try catch里面的代码是不会被编译器优化重排的。对于上面两个函数而言,只是异常表中try起点和终点位置不一样。至于刚刚说到的指令重排的问题,由于for循环条件部分符合happens- before原则,因此两者的for循环都不会发生重排。当然只是针对这里而言,在实际编程中,还是提倡try代码块的范围尽量小,这样才可以充分发挥Java对代码的优化能力。

测试验证

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

不正确的测试

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

代码语言:javascript
复制
long startTime = System.currentTimeMillis();

doReallyLongThing();

long endTime = System.currentTimeMillis();

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

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

代码语言:javascript
复制
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");

    }
}

测试结果

测试结果
测试结果

看到这个你是不是会认为tryFor比forTry快了呢?当然一般而言,不会这么快就下定论,所以接着你就会继续运行数次,然后可能还是会看见类似上面的结果。很有可能你就会确信forTry比tryFor快。但是这个测试的结果是不准确的,确切的说是无效的。

为什么呢?如果你测试的次数足够多(其实也不用很多,我这里就运行了十几次这样),你就会发现问题。我再列出这十几次测试中比较有代表性的测试结果的截图。

图1

图1
图1

图2

这里写图片描述
这里写图片描述

图3

这里写图片描述
这里写图片描述

从上面结果看来,绝大多数时候,tryFor比forTry快。那是不是可以说tryFor比forTry快了呢?如果没有前面分析,如果你只是测试了几次,并且结果都类似的时候,你是不是会就因此下定论了呢?

上面问题就出现在这个绝大多数,当你运行的次数越多,就越发的体会到结果的扑朔迷离。

至少有下面两点给人扑朔迷离的感觉
  1. 每次的执行时间都相差很大。 同一个函数会出现,两次执行结果可能相差好几倍的情况。比如图1中的forTry竟然比图2的forTry快了近6倍。
  2. 偶尔forTry会比tryFor快(我上面的截取的是比较有代表性的结果,实际运行的时候绝大多数情况显示的是tryFor快)
那是什么导致了结果如此的扑朔迷离?原因至少有下面这些

System.currentTimeMillis()测量的只是逝去的时间,并没有反映出cpu执行该函数真正消耗的时间。 这导致线程未被分配cpu资源时,等待cpu的时间也会被计算进去

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

从上面分析的原因不难看出,为什么绝大多数时候tryFor会比forTry快了。JIT编译耗时和类加载时间会被统计到第一个执行的函数forTry里面。这就直接导致了第一个执行的函数(forTry)要比第二个函数(tryFor)执行的时间要长。最为重要的使用System.currentTimeMillis()测量的是(等待cpu+真正被执行的时间),这就导致出现图1完全与绝大多数测试结果完全相反的情况。

那有什么可以让我们穿过这层层迷雾,直抵真相呢?

穿透迷雾,直抵真相!
  1. 不要使用System.currentTimeMillis()亦或者使用System.nanoTime() 这里说明一下,可能你会看到有些建议使用System.nanoTime()来测试,但是它跟System.currentTimeMillis()区别,仅仅在于时间的基准不同和精度不同,但都表示的是逝去的时间,所以对于测试执行时间上,并没有什么区别。因为都无法统计CPU真正执行时间。 要测试cpu真正执行时间,这里推荐使用JProfiler性能测试工具,它可以测量出cpu真正的执行时间。具体安装使用方法可以自行google百度。因为这不是本文最终使用的测试方法,所以就不做详细介绍了。但是你使用它来测试上面的代码,至少可以排除等待CPU消耗的时间
  2. 对于后两者,需要加入Warmup(预热)阶段。 预热阶段就是不断运行你的测试代码,从而使得代码完成初始化工作(类加载),并足以触发JIT编译机制。一般来说,循环几万次就可以预热完毕。

那是不是做到以上两点就可以了直抵真相了?非常不幸,并没有那么简单,JIT机制和JVM并没有想象的这么简单,要做到以下这些点你才能得到比较真实的结果。下面摘录至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的相同人员开发的。结果可信度很高。

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

正确的测试

测试环境:

代码语言:javascript
复制
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项目地址,上面有介绍使用细节

测试代码:

代码语言:javascript
复制
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,之后才开始进行测试。这里只是截取结果部分,运行过程输出就不放出来了

代码语言:javascript
复制
# 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
代码语言:javascript
复制
# 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函数,第二个结果表示的是调用一次函数的时间。

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

最终总结

本文由Try catch与for循环的位置关系开始讨论,通过分析得出了结论,并最终通过测试,验证了分析的结论——两者在没有抛出异常时,是没有区别的。在分析的过程中,我们也了解到try catch的实质,就是跟方法关联的异常表,在抛出异常的时候,这个就决定了异常是否会被该方法处理。

最后回到标题讨论的,try catch对性能的影响。try catch对性能还是有一定的影响,那就是try块会阻止java的优化(例如重排序)。当然重排序是需要一定的条件触发。一般而言,只要try块范围越小,对java的优化机制的影响是就越小。所以保证try块范围尽量只覆盖抛出异常的地方,就可以使得异常对java优化的机制的影响最小化。

还是那句话,先保证代码正确执行,然后在出现明显的性能问题时,再去考虑优化。

参考链接

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

发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/148726.html原文链接:https://javaforall.cn

本文参与?腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客?前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与?腾讯云自媒体同步曝光计划? ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 引言
  • 讨论的问题
  • 结论
  • 分析
  • 测试验证
    • 不正确的测试
      • 至少有下面两点给人扑朔迷离的感觉:
      • 那是什么导致了结果如此的扑朔迷离?原因至少有下面这些
      • 穿透迷雾,直抵真相!
  • 正确的测试
    • 测试环境:
      • 测试代码:
        • 测试结果
        • 最终总结
        • 参考链接
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
        http://www.vxiaotou.com