这是我的第 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 榜。
关注公众号发送”进群“,老王拉你进读者群。