这是我的第 57 篇原创文章

条件判断语句是程序的重要组成部分,也是系统业务逻辑的控制手段。重要程度和使用频率更是首屈一指,那我们要如何选择 if 还是 switch 呢?他们的性能差别有多大?switch 性能背后的秘密是什么?接下来让我们一起来寻找这些问题的答案。

switch VS if

我在之前的文章《9个小技巧让你的 if else看起来更优雅》中有提过,要尽量使用 switch 因为他的性能比较高,但具体高多少?以及为什么高的原因将在本文为你揭晓。

我们依然借助 Oracle 官方提供的 JMH(Java Microbenchmark Harness,JAVA 微基准测试套件)框架来进行测试,首先引入 JMH 框架,在 pom.xml 文件中添加如下配置:

<!-- https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-core -->
<dependency><groupId>org.openjdk.jmh</groupId><artifactId>jmh-core</artifactId><version>1.23</version>
</dependency>

然后编写测试代码,我们这里添加 5 个条件判断分支,具体实现代码如下:

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 java.util.concurrent.TimeUnit;@BenchmarkMode(Mode.AverageTime) // 测试完成时间
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS) // 预热 2 轮,每次 1s
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) // 测试 5 轮,每次 3s
@Fork(1) // fork 1 个线程
@State(Scope.Thread) // 每个测试线程一个实例
public class SwitchOptimizeTest {static Integer _NUM = 9;public static void main(String[] args) throws RunnerException {// 启动基准测试Options opt = new OptionsBuilder().include(SwitchOptimizeTest.class.getSimpleName()) // 要导入的测试类.output("/Users/admin/Desktop/jmh-switch.log") // 输出测试结果的文件.build();new Runner(opt).run(); // 执行测试}@Benchmarkpublic void switchTest() {int num1;switch (_NUM) {case 1:num1 = 1;break;case 3:num1 = 3;break;case 5:num1 = 5;break;case 7:num1 = 7;break;case 9:num1 = 9;break;default:num1 = -1;break;}}@Benchmarkpublic void ifTest() {int num1;if (_NUM == 1) {num1 = 1;} else if (_NUM == 3) {num1 = 3;} else if (_NUM == 5) {num1 = 5;} else if (_NUM == 7) {num1 = 7;} else if (_NUM == 9) {num1 = 9;} else {num1 = -1;}}
}

以上代码的测试结果如下:


备注:本文的测试环境为:JDK 1.8 / Mac mini (2018) / Idea 2020.1

从以上结果可以看出(Score 列),switch 的平均执行完成时间比 if 的平均执行完成时间快了约 2.33 倍

性能分析

为什么 switch 的性能会比 if 的性能高这么多?

这需要从他们字节码说起,我们把他们的代码使用 javac 生成字节码如下所示:

public class com.example.optimize.SwitchOptimize {static java.lang.Integer _NUM;public com.example.optimize.SwitchOptimize();Code:0: aload_01: invokespecial #1                  // Method java/lang/Object."<init>":()V4: returnpublic static void main(java.lang.String[]);Code:0: invokestatic  #7                  // Method switchTest:()V3: invokestatic  #12                 // Method ifTest:()V6: returnpublic static void switchTest();Code:0: getstatic     #15                 // Field _NUM:Ljava/lang/Integer;3: invokevirtual #19                 // Method java/lang/Integer.intValue:()I6: tableswitch   { // 1 to 91: 562: 833: 614: 835: 666: 837: 718: 839: 77default: 83}56: iconst_157: istore_058: goto          8561: iconst_362: istore_063: goto          8566: iconst_567: istore_068: goto          8571: bipush        773: istore_074: goto          8577: bipush        979: istore_080: goto          8583: iconst_m184: istore_085: returnpublic static void ifTest();Code:0: getstatic     #15                 // Field _NUM:Ljava/lang/Integer;3: invokevirtual #19                 // Method java/lang/Integer.intValue:()I6: iconst_17: if_icmpne     1510: iconst_111: istore_012: goto          8115: getstatic     #15                 // Field _NUM:Ljava/lang/Integer;18: invokevirtual #19                 // Method java/lang/Integer.intValue:()I21: iconst_322: if_icmpne     3025: iconst_326: istore_027: goto          8130: getstatic     #15                 // Field _NUM:Ljava/lang/Integer;33: invokevirtual #19                 // Method java/lang/Integer.intValue:()I36: iconst_537: if_icmpne     4540: iconst_541: istore_042: goto          8145: getstatic     #15                 // Field _NUM:Ljava/lang/Integer;48: invokevirtual #19                 // Method java/lang/Integer.intValue:()I51: bipush        753: if_icmpne     6256: bipush        758: istore_059: goto          8162: getstatic     #15                 // Field _NUM:Ljava/lang/Integer;65: invokevirtual #19                 // Method java/lang/Integer.intValue:()I68: bipush        970: if_icmpne     7973: bipush        975: istore_076: goto          8179: iconst_m180: istore_081: returnstatic {};Code:0: iconst_11: invokestatic  #25                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;4: putstatic     #15                 // Field _NUM:Ljava/lang/Integer;7: return
}

这些字节码中最重要的信息是“getstatic     #15”,这段代码表示取出“_NUM”变量和条件进行判断。

从上面的字节码可以看出,在 switch 中只取出了一次变量和条件进行比较,而 if 中每次都会取出变量和条件进行比较,因此 if 的效率就会比 switch 慢很多

提升测试量

前面的测试代码我们使用了 5 个分支条件来测试了 if 和 switch 的性能,那如果把分支的判断条件增加 3 倍(15 个)时,测试的结果又会怎么呢?

增加至 15 个分支判断的实现代码如下:

package com.example.optimize;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 java.util.concurrent.TimeUnit;@BenchmarkMode(Mode.AverageTime) // 测试完成时间
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS) // 预热 2 轮,每次 1s
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) // 测试 5 轮,每次 3s
@Fork(1) // fork 1 个线程
@State(Scope.Thread) // 每个测试线程一个实例
public class SwitchOptimizeTest {static Integer _NUM = 1;public static void main(String[] args) throws RunnerException {// 启动基准测试Options opt = new OptionsBuilder().include(SwitchOptimizeTest.class.getSimpleName()) // 要导入的测试类.output("/Users/admin/Desktop/jmh-switch.log") // 输出测试结果的文件.build();new Runner(opt).run(); // 执行测试}@Benchmarkpublic void switchTest() {int num1;switch (_NUM) {case 1:num1 = 1;break;case 2:num1 = 2;break;case 3:num1 = 3;break;case 4:num1 = 4;break;case 5:num1 = 5;break;case 6:num1 = 6;break;case 7:num1 = 7;break;case 8:num1 = 8;break;case 9:num1 = 9;break;case 10:num1 = 10;break;case 11:num1 = 11;break;case 12:num1 = 12;break;case 13:num1 = 13;break;case 14:num1 = 14;break;case 15:num1 = 15;break;default:num1 = -1;break;}}@Benchmarkpublic void ifTest() {int num1;if (_NUM == 1) {num1 = 1;} else if (_NUM == 2) {num1 = 2;} else if (_NUM == 3) {num1 = 3;} else if (_NUM == 4) {num1 = 4;} else if (_NUM == 5) {num1 = 5;} else if (_NUM == 6) {num1 = 6;} else if (_NUM == 7) {num1 = 7;} else if (_NUM == 8) {num1 = 8;} else if (_NUM == 9) {num1 = 9;} else if (_NUM == 10) {num1 = 10;} else if (_NUM == 11) {num1 = 11;} else if (_NUM == 12) {num1 = 12;} else if (_NUM == 13) {num1 = 13;} else if (_NUM == 14) {num1 = 14;} else if (_NUM == 15) {num1 = 15;} else {num1 = -1;}}
}

以上代码的测试结果如下:


从 Score 的值可以看出,当分支判断增加至 15 个,switch 的性能比 if 的性能高出了约 3.7 倍,而之前有 5 个分支判断时的测试结果为,switch 的性能比 if 的性能高出了约 2.3 倍,也就是说分支的判断条件越多,switch 性能高的特性体现的就越明显

switch 的秘密

对于 switch 来说,他最终生成的字节码有两种形态,一种是 tableswitch,另一种是 lookupswitch,决定最终生成的代码使用那种形态取决于 switch 的判断添加是否紧凑,例如到 case 是 1...2...3...4 这种依次递增的判断条件时,使用的是 tableswitch,而像 case 是 1...33...55...22 这种非紧凑型的判断条件时则会使用 lookupswitch,测试代码如下:

public class SwitchOptimize {static Integer _NUM = 1;public static void main(String[] args) {tableSwitchTest();lookupSwitchTest();}public static void tableSwitchTest() {int num1;switch (_NUM) {case 1:num1 = 1;break;case 2:num1 = 2;break;case 3:num1 = 3;break;case 4:num1 = 4;break;case 5:num1 = 5;break;case 6:num1 = 6;break;case 7:num1 = 7;break;case 8:num1 = 8;break;case 9:num1 = 9;break;default:num1 = -1;break;}}public static void lookupSwitchTest() {int num1;switch (_NUM) {case 1:num1 = 1;break;case 11:num1 = 2;break;case 3:num1 = 3;break;case 4:num1 = 4;break;case 19:num1 = 5;break;case 6:num1 = 6;break;case 33:num1 = 7;break;case 8:num1 = 8;break;case 999:num1 = 9;break;default:num1 = -1;break;}}
}

对应的字节码如下:

public class com.example.optimize.SwitchOptimize {static java.lang.Integer _NUM;public com.example.optimize.SwitchOptimize();Code:0: aload_01: invokespecial #1                  // Method java/lang/Object."<init>":()V4: returnpublic static void main(java.lang.String[]);Code:0: invokestatic  #7                  // Method tableSwitchTest:()V3: invokestatic  #12                 // Method lookupSwitchTest:()V6: returnpublic static void tableSwitchTest();Code:0: getstatic     #15                 // Field _NUM:Ljava/lang/Integer;3: invokevirtual #19                 // Method java/lang/Integer.intValue:()I6: tableswitch   { // 1 to 91: 562: 613: 664: 715: 766: 817: 878: 939: 99default: 105}56: iconst_157: istore_058: goto          10761: iconst_262: istore_063: goto          10766: iconst_367: istore_068: goto          10771: iconst_472: istore_073: goto          10776: iconst_577: istore_078: goto          10781: bipush        683: istore_084: goto          10787: bipush        789: istore_090: goto          10793: bipush        895: istore_096: goto          10799: bipush        9101: istore_0102: goto          107105: iconst_m1106: istore_0107: returnpublic static void lookupSwitchTest();Code:0: getstatic     #15                 // Field _NUM:Ljava/lang/Integer;3: invokevirtual #19                 // Method java/lang/Integer.intValue:()I6: lookupswitch  { // 91: 883: 984: 1036: 1138: 12511: 9319: 10833: 119999: 131default: 137}88: iconst_189: istore_090: goto          13993: iconst_294: istore_095: goto          13998: iconst_399: istore_0100: goto          139103: iconst_4104: istore_0105: goto          139108: iconst_5109: istore_0110: goto          139113: bipush        6115: istore_0116: goto          139119: bipush        7121: istore_0122: goto          139125: bipush        8127: istore_0128: goto          139131: bipush        9133: istore_0134: goto          139137: iconst_m1138: istore_0139: returnstatic {};Code:0: iconst_11: invokestatic  #25                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;4: putstatic     #15                 // Field _NUM:Ljava/lang/Integer;7: return
}

从上面字节码可以看出 tableSwitchTest 使用的 tableswitch,而 lookupSwitchTest 则是使用的 lookupswitch。

tableswitch VS lookupSwitchTest

当执行一次 tableswitch 时,堆栈顶部的 int 值直接用作表中的索引,以便抓取跳转目标并立即执行跳转。也就是说 tableswitch 的存储结构类似于数组,是直接用索引获取元素的,所以整个查询的时间复杂度是 O(1),这也意味着它的搜索速度非常快。

而执行 lookupswitch 时,会逐个进行分支比较或者使用二分法进行查询,因此查询时间复杂度是 O(log n),所以使用 lookupswitch 会比 tableswitch 慢

接下来我们使用实际的代码测试一下,他们两个之间的性能,测试代码如下:

package com.example.optimize;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 java.util.concurrent.TimeUnit;@BenchmarkMode(Mode.AverageTime) // 测试完成时间
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS) // 预热 2 轮,每次 1s
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) // 测试 5 轮,每次 3s
@Fork(1) // fork 1 个线程
@State(Scope.Thread) // 每个测试线程一个实例
public class SwitchOptimizeTest {static Integer _NUM = -1;public static void main(String[] args) throws RunnerException {// 启动基准测试Options opt = new OptionsBuilder().include(SwitchOptimizeTest.class.getSimpleName()) // 要导入的测试类.build();new Runner(opt).run(); // 执行测试}@Benchmarkpublic void tableSwitchTest() {int num1;switch (_NUM) {case 1:num1 = 1;break;case 2:num1 = 2;break;case 3:num1 = 3;break;case 4:num1 = 4;break;case 5:num1 = 5;break;case 6:num1 = 6;break;case 7:num1 = 7;break;case 8:num1 = 8;break;case 9:num1 = 9;break;default:num1 = -1;break;}}@Benchmarkpublic void lookupSwitchTest() {int num1;switch (_NUM) {case 1:num1 = 1;break;case 11:num1 = 2;break;case 3:num1 = 3;break;case 4:num1 = 4;break;case 19:num1 = 5;break;case 6:num1 = 6;break;case 33:num1 = 7;break;case 8:num1 = 8;break;case 999:num1 = 9;break;default:num1 = -1;break;}}
}

以上代码的测试结果如下:


可以看出在分支判断为 9 个时,tableswitch 的性能比 lookupwitch 的性能快了约 1.3 倍。但即使这样 lookupwitch 依然比 if 查询性能要高很多

总结

switch 的判断条件是 5 个时,性能比 if 高出了约 2.3 倍,而当判断条件的数量越多时,他们的性能相差就越大。而 switch 在编译为字节码时,会根据 switch 的判断条件是否紧凑生成两种代码:tableswitch(紧凑时生成)和 lookupswitch(非紧凑时生成),其中 tableswitch 是采用类似于数组的存储结构,直接根据索引查询元素;而 lookupswitch 则需要逐个查询或者使用二分法查询,因此 tableswitch 的性能会比 lookupswitch 的性能高,但无论如何 switch 的性能都比 if 的性能要高

最后的话

原创不易,如果觉得本文对你有用,请随手点击一个「」,这是对作者最大的支持与鼓励,谢谢你。

参考 & 鸣谢

https://www.javaguides.net/2020/03/5-best-ways-to-iterate-over-hashmap-in-java.html

HashMap 的 7 种遍历方式与性能分析!「修正篇」

String性能提升10倍的几个方法!(源码+原理分析)

关注公众号「Java中文社群」回复“干货”,获取原创干货 Top 榜

关注公众号发送”进群“,老王拉你进读者群。

if快还是switch快?解密switch背后的秘密相关推荐

  1. if快还是switch快?

    转自:微点阅读(www.weidianyuedu.com)微点阅读 - 范文大全 - 免费学习知识的网站 来源:Java中文社群 条件判断语句是程序的重要组成部分,也是系统业务逻辑的控制手段.重要程度 ...

  2. 谈谈我对SEO快排现象的观察及其背后原理的分析

    对于个人站长来说,最苦逼的事情莫过于明明很努力的去做了却很难得到应有的结果.尤其是在查自己网站排名的时候往往会遇到这样一种现象:我们自己很用心去优化的网站,坚持更新原创文章,坚持发外链,坚持提交链接, ...

  3. 快手电商“大搞产业带”背后

    出品 | 何玺 排版 | 叶媛 在过去的两年中,如果要说中国互联网行业最火的关键词是什么,那么结果毫无疑问一定是"直播带货".作为一种全新的电商模式,直播带货几乎可以说赋予了互联网 ...

  4. 读的, 且经过美化, 能在所有 JavaScript 环境中运行, 并且应该和对应手写的 JavaScript 一样快或者更快.

    目录 试一试 CoffeeScript 代码和注释 CoffeeScript 是一门编译到 JavaScript 的小巧语言. 在 Java 般笨拙的外表下, JavaScript 其实有着一颗华丽的 ...

  5. 比最快的超级计算机快一百万亿倍!中国科学家实现“量子计算优越性”里程碑

    本文来自:中国科学技术大学公众号 北京时间12月4日国际顶尖杂志<Science>刊发了中国科学技术大学潘建伟.陆朝阳等组成的研究团队的一项重磅研究成果 让我们一起来看看吧! 中国科学家实 ...

  6. 比最快的超级计算机快一百万亿倍!中国科学家实现“量子计算优越性”里程碑...

    北京时间12月4日 国际顶尖杂志<Science>刊发了 中国科学技术大学 潘建伟.陆朝阳等组成的研究团队 的一项重磅研究成果 让我们一起来看看吧! 中国科学家实现 "量子计算优 ...

  7. android 快应用原理,快应用初探——写一个快应用练练手。

    快应用是九大手机厂商基于硬件平台共同推出的新型应用生态.用户无需下载安装,即点即用,享受原生应用的性能体验.研究了一个星期的快应用之后,然后写了一个简单的快应用,原本是想做一个玩Android的快应用 ...

  8. 中国九章量子计算机诞生!比最快的超算快一百万亿倍

    所谓"科学技术是第一生产力",近年来中国的崛起,离不开科技创新和技术创新.近日,中国科学技术大学潘建伟研究团队与中科院上海微系统所.国家并行计算机工程技术研究中心合作,成功研制出量 ...

  9. 网络:TCP停止等待、超时重传、滑动窗口、拥塞控制、快重传和快恢复

    TCP超时与重传机制 TCP协议是一种面向连接的可靠的传输层协议,它保证了数据的可靠传输,对于一些出错,超时丢包等问题TCP设计的超时与重传机制.其基本原理:在发送一个数据之后,就开启一个定时器,若是 ...

最新文章

  1. 快速开发基于 HTML5 网络拓扑图应用--入门篇(二)
  2. python的官方网站地址是什么-python赋值和地址
  3. eDrawings Pro 2020中文版
  4. 图论与java_算法笔记_150:图论之双连通及桥的应用(Java)
  5. 放弃手机后!罗永浩今年已带货19亿元
  6. tcp重复的确认_TCP如何实现可靠性传输
  7. CentOS7 基于http服务搭建本地yum仓库
  8. 文玩扇子(折扇)的寸、方、排口、头分别指什么?
  9. jQuery.Deferred exception: e.indexOf
  10. 2018想要薪资翻倍?你需要掌握这个技能
  11. 【Netty - 解码器】did not read anything but decoded a message 异常
  12. 【机器学习课程】 第一章机器学习概述 1.人工智能
  13. oracle限定词,ORACLE诊断事件
  14. 电传输之POE供电的介绍
  15. docker修炼手册
  16. python数据分析与挖掘学习笔记(7)-交通路标自动识别实战与神经网络算法
  17. 国内生产总值(GDP)数据可视化
  18. Python实现《合成孔径雷达成像——算法与实现》图2.8和2.9
  19. 复习总结:模拟电子技术(模电)
  20. HDU 1215 七夕节

热门文章

  1. 51CTO会员开通成功!开森!
  2. Redis详解(三)
  3. 湖南多校对抗5.24
  4. 安装TPCC-MySQL报错
  5. 10大iOS开发者最喜爱的类库
  6. uva 610(tarjan的应用)
  7. Visual Studio 2010快捷键大全
  8. java log输出到文件路径_Java - 配置log4j的日志文件路径 (附-获取当前类路径的多种方法)...
  9. Tomcat log文件
  10. 查看进程占用,并kill掉