飞道的博客

if快还是switch快?解密switch背后的秘密

236人阅读  评论(0)

这是我的第 57 篇原创文章

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

switch VS if

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

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


   
  1. <!-- https: //mvnrepository.com/artifact/org.openjdk.jmh/jmh-core -->
  2. <dependency>
  3.    <groupId>org.openjdk.jmh</groupId>
  4.    <artifactId>jmh-core</artifactId>
  5.    <version> 1.23</version>
  6. </dependency>

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


   
  1. import org.openjdk.jmh.annotations.*;
  2. import org.openjdk.jmh.runner.Runner;
  3. import org.openjdk.jmh.runner.RunnerException;
  4. import org.openjdk.jmh.runner.options.Options;
  5. import org.openjdk.jmh.runner.options.OptionsBuilder;
  6. import java.util.concurrent.TimeUnit;
  7. @BenchmarkMode(Mode.AverageTime)  // 测试完成时间
  8. @OutputTimeUnit(TimeUnit.NANOSECONDS)
  9. @Warmup(iterations =  2, time =  1, timeUnit = TimeUnit.SECONDS)  // 预热 2 轮,每次 1s
  10. @Measurement(iterations =  5, time =  1, timeUnit = TimeUnit.SECONDS)  // 测试 5 轮,每次 3s
  11. @Fork( 1// fork 1 个线程
  12. @State(Scope.Thread)  // 每个测试线程一个实例
  13. public class SwitchOptimizeTest {
  14.     static Integer _NUM =  9;
  15.     public static void main(String[] args) throws RunnerException {
  16.          // 启动基准测试
  17.         Options opt =  new OptionsBuilder()
  18.                 .include(SwitchOptimizeTest.class.getSimpleName())  // 要导入的测试类
  19.                 .output( "/Users/admin/Desktop/jmh-switch.log"// 输出测试结果的文件
  20.                 .build();
  21.          new Runner(opt).run();  // 执行测试
  22.     }
  23.     @Benchmark
  24.     public void switchTest() {
  25.          int num1;
  26.          switch (_NUM) {
  27.              case  1:
  28.                 num1 =  1;
  29.                  break;
  30.              case  3:
  31.                 num1 =  3;
  32.                  break;
  33.              case  5:
  34.                 num1 =  5;
  35.                  break;
  36.              case  7:
  37.                 num1 =  7;
  38.                  break;
  39.              case  9:
  40.                 num1 =  9;
  41.                  break;
  42.              default:
  43.                 num1 =  -1;
  44.                  break;
  45.         }
  46.     }
  47.     @Benchmark
  48.     public void ifTest() {
  49.          int num1;
  50.          if (_NUM ==  1) {
  51.             num1 =  1;
  52.         }  else  if (_NUM ==  3) {
  53.             num1 =  3;
  54.         }  else  if (_NUM ==  5) {
  55.             num1 =  5;
  56.         }  else  if (_NUM ==  7) {
  57.             num1 =  7;
  58.         }  else  if (_NUM ==  9) {
  59.             num1 =  9;
  60.         }  else {
  61.             num1 =  -1;
  62.         }
  63.     }
  64. }

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


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

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

性能分析

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

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


   
  1. public class com.example.optimize.SwitchOptimize {
  2.   static java.lang.Integer _NUM;
  3.   public com.example.optimize.SwitchOptimize();
  4.     Code:
  5.         0: aload_0
  6.         1: invokespecial # 1                   // Method java/lang/Object."<init>":()V
  7.         4return
  8.   public static void main(java.lang.String[]);
  9.     Code:
  10.         0: invokestatic  # 7                   // Method switchTest:()V
  11.         3: invokestatic  # 12                  // Method ifTest:()V
  12.         6return
  13.   public static void switchTest();
  14.     Code:
  15.         0: getstatic     # 15                  // Field _NUM:Ljava/lang/Integer;
  16.         3: invokevirtual # 19                  // Method java/lang/Integer.intValue:()I
  17.         6: tableswitch   {  // 1 to 9
  18.                       156
  19.                       283
  20.                       361
  21.                       483
  22.                       566
  23.                       683
  24.                       771
  25.                       883
  26.                       977
  27.                 default83
  28.           }
  29.        56: iconst_1
  30.        57: istore_0
  31.        58goto           85
  32.        61: iconst_3
  33.        62: istore_0
  34.        63goto           85
  35.        66: iconst_5
  36.        67: istore_0
  37.        68goto           85
  38.        71: bipush         7
  39.        73: istore_0
  40.        74goto           85
  41.        77: bipush         9
  42.        79: istore_0
  43.        80goto           85
  44.        83: iconst_m1
  45.        84: istore_0
  46.        85return
  47.   public static void ifTest();
  48.     Code:
  49.         0: getstatic     # 15                  // Field _NUM:Ljava/lang/Integer;
  50.         3: invokevirtual # 19                  // Method java/lang/Integer.intValue:()I
  51.         6: iconst_1
  52.         7: if_icmpne      15
  53.        10: iconst_1
  54.        11: istore_0
  55.        12goto           81
  56.        15: getstatic     # 15                  // Field _NUM:Ljava/lang/Integer;
  57.        18: invokevirtual # 19                  // Method java/lang/Integer.intValue:()I
  58.        21: iconst_3
  59.        22: if_icmpne      30
  60.        25: iconst_3
  61.        26: istore_0
  62.        27goto           81
  63.        30: getstatic     # 15                  // Field _NUM:Ljava/lang/Integer;
  64.        33: invokevirtual # 19                  // Method java/lang/Integer.intValue:()I
  65.        36: iconst_5
  66.        37: if_icmpne      45
  67.        40: iconst_5
  68.        41: istore_0
  69.        42goto           81
  70.        45: getstatic     # 15                  // Field _NUM:Ljava/lang/Integer;
  71.        48: invokevirtual # 19                  // Method java/lang/Integer.intValue:()I
  72.        51: bipush         7
  73.        53: if_icmpne      62
  74.        56: bipush         7
  75.        58: istore_0
  76.        59goto           81
  77.        62: getstatic     # 15                  // Field _NUM:Ljava/lang/Integer;
  78.        65: invokevirtual # 19                  // Method java/lang/Integer.intValue:()I
  79.        68: bipush         9
  80.        70: if_icmpne      79
  81.        73: bipush         9
  82.        75: istore_0
  83.        76goto           81
  84.        79: iconst_m1
  85.        80: istore_0
  86.        81return
  87.   static {};
  88.     Code:
  89.         0: iconst_1
  90.         1: invokestatic  # 25                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
  91.         4: putstatic     # 15                  // Field _NUM:Ljava/lang/Integer;
  92.         7return
  93. }

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

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

提升测试量

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

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


   
  1. package com.example.optimize;
  2. import org.openjdk.jmh.annotations.*;
  3. import org.openjdk.jmh.runner.Runner;
  4. import org.openjdk.jmh.runner.RunnerException;
  5. import org.openjdk.jmh.runner.options.Options;
  6. import org.openjdk.jmh.runner.options.OptionsBuilder;
  7. import java.util.concurrent.TimeUnit;
  8. @BenchmarkMode(Mode.AverageTime)  // 测试完成时间
  9. @OutputTimeUnit(TimeUnit.NANOSECONDS)
  10. @Warmup(iterations =  2, time =  1, timeUnit = TimeUnit.SECONDS)  // 预热 2 轮,每次 1s
  11. @Measurement(iterations =  5, time =  1, timeUnit = TimeUnit.SECONDS)  // 测试 5 轮,每次 3s
  12. @Fork( 1// fork 1 个线程
  13. @State(Scope.Thread)  // 每个测试线程一个实例
  14. public class SwitchOptimizeTest {
  15.     static Integer _NUM =  1;
  16.     public static void main(String[] args) throws RunnerException {
  17.          // 启动基准测试
  18.         Options opt =  new OptionsBuilder()
  19.                 .include(SwitchOptimizeTest.class.getSimpleName())  // 要导入的测试类
  20.                 .output( "/Users/admin/Desktop/jmh-switch.log"// 输出测试结果的文件
  21.                 .build();
  22.          new Runner(opt).run();  // 执行测试
  23.     }
  24.     @Benchmark
  25.     public void switchTest() {
  26.          int num1;
  27.          switch (_NUM) {
  28.              case  1:
  29.                 num1 =  1;
  30.                  break;
  31.              case  2:
  32.                 num1 =  2;
  33.                  break;
  34.              case  3:
  35.                 num1 =  3;
  36.                  break;
  37.              case  4:
  38.                 num1 =  4;
  39.                  break;
  40.              case  5:
  41.                 num1 =  5;
  42.                  break;
  43.              case  6:
  44.                 num1 =  6;
  45.                  break;
  46.              case  7:
  47.                 num1 =  7;
  48.                  break;
  49.              case  8:
  50.                 num1 =  8;
  51.                  break;
  52.              case  9:
  53.                 num1 =  9;
  54.                  break;
  55.              case  10:
  56.                 num1 =  10;
  57.                  break;
  58.              case  11:
  59.                 num1 =  11;
  60.                  break;
  61.              case  12:
  62.                 num1 =  12;
  63.                  break;
  64.              case  13:
  65.                 num1 =  13;
  66.                  break;
  67.              case  14:
  68.                 num1 =  14;
  69.                  break;
  70.              case  15:
  71.                 num1 =  15;
  72.                  break;
  73.              default:
  74.                 num1 =  -1;
  75.                  break;
  76.         }
  77.     }
  78.     @Benchmark
  79.     public void ifTest() {
  80.          int num1;
  81.          if (_NUM ==  1) {
  82.             num1 =  1;
  83.         }  else  if (_NUM ==  2) {
  84.             num1 =  2;
  85.         }  else  if (_NUM ==  3) {
  86.             num1 =  3;
  87.         }  else  if (_NUM ==  4) {
  88.             num1 =  4;
  89.         }  else  if (_NUM ==  5) {
  90.             num1 =  5;
  91.         }  else  if (_NUM ==  6) {
  92.             num1 =  6;
  93.         }  else  if (_NUM ==  7) {
  94.             num1 =  7;
  95.         }  else  if (_NUM ==  8) {
  96.             num1 =  8;
  97.         }  else  if (_NUM ==  9) {
  98.             num1 =  9;
  99.         }  else  if (_NUM ==  10) {
  100.             num1 =  10;
  101.         }  else  if (_NUM ==  11) {
  102.             num1 =  11;
  103.         }  else  if (_NUM ==  12) {
  104.             num1 =  12;
  105.         }  else  if (_NUM ==  13) {
  106.             num1 =  13;
  107.         }  else  if (_NUM ==  14) {
  108.             num1 =  14;
  109.         }  else  if (_NUM ==  15) {
  110.             num1 =  15;
  111.         }  else {
  112.             num1 =  -1;
  113.         }
  114.     }
  115. }

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


从 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,测试代码如下:


   
  1. public class SwitchOptimize {
  2.     static Integer _NUM =  1;
  3.     public static void main(String[] args) {
  4.         tableSwitchTest();
  5.         lookupSwitchTest();
  6.     }
  7.     public static void tableSwitchTest() {
  8.          int num1;
  9.          switch (_NUM) {
  10.              case  1:
  11.                 num1 =  1;
  12.                  break;
  13.              case  2:
  14.                 num1 =  2;
  15.                  break;
  16.              case  3:
  17.                 num1 =  3;
  18.                  break;
  19.              case  4:
  20.                 num1 =  4;
  21.                  break;
  22.              case  5:
  23.                 num1 =  5;
  24.                  break;
  25.              case  6:
  26.                 num1 =  6;
  27.                  break;
  28.              case  7:
  29.                 num1 =  7;
  30.                  break;
  31.              case  8:
  32.                 num1 =  8;
  33.                  break;
  34.              case  9:
  35.                 num1 =  9;
  36.                  break;
  37.              default:
  38.                 num1 =  -1;
  39.                  break;
  40.         }
  41.     }
  42.     public static void lookupSwitchTest() {
  43.          int num1;
  44.          switch (_NUM) {
  45.              case  1:
  46.                 num1 =  1;
  47.                  break;
  48.              case  11:
  49.                 num1 =  2;
  50.                  break;
  51.              case  3:
  52.                 num1 =  3;
  53.                  break;
  54.              case  4:
  55.                 num1 =  4;
  56.                  break;
  57.              case  19:
  58.                 num1 =  5;
  59.                  break;
  60.              case  6:
  61.                 num1 =  6;
  62.                  break;
  63.              case  33:
  64.                 num1 =  7;
  65.                  break;
  66.              case  8:
  67.                 num1 =  8;
  68.                  break;
  69.              case  999:
  70.                 num1 =  9;
  71.                  break;
  72.              default:
  73.                 num1 =  -1;
  74.                  break;
  75.         }
  76.     }
  77. }

对应的字节码如下:


   
  1. public class com.example.optimize.SwitchOptimize {
  2.   static java.lang.Integer _NUM;
  3.   public com.example.optimize.SwitchOptimize();
  4.     Code:
  5.         0: aload_0
  6.         1: invokespecial # 1                   // Method java/lang/Object."<init>":()V
  7.         4return
  8.   public static void main(java.lang.String[]);
  9.     Code:
  10.         0: invokestatic  # 7                   // Method tableSwitchTest:()V
  11.         3: invokestatic  # 12                  // Method lookupSwitchTest:()V
  12.         6return
  13.   public static void tableSwitchTest();
  14.     Code:
  15.         0: getstatic     # 15                  // Field _NUM:Ljava/lang/Integer;
  16.         3: invokevirtual # 19                  // Method java/lang/Integer.intValue:()I
  17.         6: tableswitch   {  // 1 to 9
  18.                       156
  19.                       261
  20.                       366
  21.                       471
  22.                       576
  23.                       681
  24.                       787
  25.                       893
  26.                       999
  27.                 default105
  28.           }
  29.        56: iconst_1
  30.        57: istore_0
  31.        58goto           107
  32.        61: iconst_2
  33.        62: istore_0
  34.        63goto           107
  35.        66: iconst_3
  36.        67: istore_0
  37.        68goto           107
  38.        71: iconst_4
  39.        72: istore_0
  40.        73goto           107
  41.        76: iconst_5
  42.        77: istore_0
  43.        78goto           107
  44.        81: bipush         6
  45.        83: istore_0
  46.        84goto           107
  47.        87: bipush         7
  48.        89: istore_0
  49.        90goto           107
  50.        93: bipush         8
  51.        95: istore_0
  52.        96goto           107
  53.        99: bipush         9
  54.       101: istore_0
  55.       102goto           107
  56.       105: iconst_m1
  57.       106: istore_0
  58.       107return
  59.   public static void lookupSwitchTest();
  60.     Code:
  61.         0: getstatic     # 15                  // Field _NUM:Ljava/lang/Integer;
  62.         3: invokevirtual # 19                  // Method java/lang/Integer.intValue:()I
  63.         6: lookupswitch  {  // 9
  64.                       188
  65.                       398
  66.                       4103
  67.                       6113
  68.                       8125
  69.                      1193
  70.                      19108
  71.                      33119
  72.                     999131
  73.                 default137
  74.           }
  75.        88: iconst_1
  76.        89: istore_0
  77.        90goto           139
  78.        93: iconst_2
  79.        94: istore_0
  80.        95goto           139
  81.        98: iconst_3
  82.        99: istore_0
  83.       100goto           139
  84.       103: iconst_4
  85.       104: istore_0
  86.       105goto           139
  87.       108: iconst_5
  88.       109: istore_0
  89.       110goto           139
  90.       113: bipush         6
  91.       115: istore_0
  92.       116goto           139
  93.       119: bipush         7
  94.       121: istore_0
  95.       122goto           139
  96.       125: bipush         8
  97.       127: istore_0
  98.       128goto           139
  99.       131: bipush         9
  100.       133: istore_0
  101.       134goto           139
  102.       137: iconst_m1
  103.       138: istore_0
  104.       139return
  105.   static {};
  106.     Code:
  107.         0: iconst_1
  108.         1: invokestatic  # 25                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
  109.         4: putstatic     # 15                  // Field _NUM:Ljava/lang/Integer;
  110.         7return
  111. }

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

tableswitch VS lookupSwitchTest

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

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

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


   
  1. package com.example.optimize;
  2. import org.openjdk.jmh.annotations.*;
  3. import org.openjdk.jmh.runner.Runner;
  4. import org.openjdk.jmh.runner.RunnerException;
  5. import org.openjdk.jmh.runner.options.Options;
  6. import org.openjdk.jmh.runner.options.OptionsBuilder;
  7. import java.util.concurrent.TimeUnit;
  8. @BenchmarkMode(Mode.AverageTime)  // 测试完成时间
  9. @OutputTimeUnit(TimeUnit.NANOSECONDS)
  10. @Warmup(iterations =  2, time =  1, timeUnit = TimeUnit.SECONDS)  // 预热 2 轮,每次 1s
  11. @Measurement(iterations =  5, time =  1, timeUnit = TimeUnit.SECONDS)  // 测试 5 轮,每次 3s
  12. @Fork( 1// fork 1 个线程
  13. @State(Scope.Thread)  // 每个测试线程一个实例
  14. public class SwitchOptimizeTest {
  15.     static Integer _NUM =  -1;
  16.     public static void main(String[] args) throws RunnerException {
  17.          // 启动基准测试
  18.         Options opt =  new OptionsBuilder()
  19.                 .include(SwitchOptimizeTest.class.getSimpleName())  // 要导入的测试类
  20.                 .build();
  21.          new Runner(opt).run();  // 执行测试
  22.     }
  23.     @Benchmark
  24.     public void tableSwitchTest() {
  25.          int num1;
  26.          switch (_NUM) {
  27.              case  1:
  28.                 num1 =  1;
  29.                  break;
  30.              case  2:
  31.                 num1 =  2;
  32.                  break;
  33.              case  3:
  34.                 num1 =  3;
  35.                  break;
  36.              case  4:
  37.                 num1 =  4;
  38.                  break;
  39.              case  5:
  40.                 num1 =  5;
  41.                  break;
  42.              case  6:
  43.                 num1 =  6;
  44.                  break;
  45.              case  7:
  46.                 num1 =  7;
  47.                  break;
  48.              case  8:
  49.                 num1 =  8;
  50.                  break;
  51.              case  9:
  52.                 num1 =  9;
  53.                  break;
  54.              default:
  55.                 num1 =  -1;
  56.                  break;
  57.         }
  58.     }
  59.     @Benchmark
  60.     public void lookupSwitchTest() {
  61.          int num1;
  62.          switch (_NUM) {
  63.              case  1:
  64.                 num1 =  1;
  65.                  break;
  66.              case  11:
  67.                 num1 =  2;
  68.                  break;
  69.              case  3:
  70.                 num1 =  3;
  71.                  break;
  72.              case  4:
  73.                 num1 =  4;
  74.                  break;
  75.              case  19:
  76.                 num1 =  5;
  77.                  break;
  78.              case  6:
  79.                 num1 =  6;
  80.                  break;
  81.              case  33:
  82.                 num1 =  7;
  83.                  break;
  84.              case  8:
  85.                 num1 =  8;
  86.                  break;
  87.              case  999:
  88.                 num1 =  9;
  89.                  break;
  90.              default:
  91.                 num1 =  -1;
  92.                  break;
  93.         }
  94.     }
  95. }

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


可以看出在分支判断为 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 榜

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


转载:https://blog.csdn.net/sufu1065/article/details/106010345
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场