為什麼大家都喜歡用if?難道是if比switch快?

java領域佼佼者 發佈 2020-05-12T01:49:33+00:00

以下文章來源於Java中文社群 ,作者磊哥專注於Java領域技術,歡迎關注條件判斷語句是程序的重要組成部分,也是系統業務邏輯的控制手段。重要程度和使用頻率更是首屈一指,那我們要如何選擇 if 還是 switch 呢?他們的性能差別有多大?switch 性能背後的秘密是什麼?

以下文章來源於Java中文社群 ,作者磊哥

專注於Java領域技術,歡迎關注
條件判斷語句是程序的重要組成部分,也是系統業務邏輯的控制手段。重要程度和使用頻率更是首屈一指,那我們要如何選擇 if 還是 switch 呢?他們的性能差別有多大?switch 性能背後的秘密是什麼?接下來讓我們一起來尋找這些問題的答案。

switch VS if

我們依然藉助 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(); // 執行測試
    }

    @Benchmark
    public 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;
        }
    }

    @Benchmark
    public 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_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: invokestatic  #7                  // Method switchTest:()V
       3: invokestatic  #12                 // Method ifTest:()V
       6: return

  public static void switchTest();
    Code:
       0: getstatic     #15                 // Field _NUM:Ljava/lang/Integer;
       3: invokevirtual #19                 // Method java/lang/Integer.intValue:()I
       6: tableswitch   { // 1 to 9
                     1: 56
                     2: 83
                     3: 61
                     4: 83
                     5: 66
                     6: 83
                     7: 71
                     8: 83
                     9: 77
               default: 83
          }
      56: iconst_1
      57: istore_0
      58: goto          85
      61: iconst_3
      62: istore_0
      63: goto          85
      66: iconst_5
      67: istore_0
      68: goto          85
      71: bipush        7
      73: istore_0
      74: goto          85
      77: bipush        9
      79: istore_0
      80: goto          85
      83: iconst_m1
      84: istore_0
      85: return

  public static void ifTest();
    Code:
       0: getstatic     #15                 // Field _NUM:Ljava/lang/Integer;
       3: invokevirtual #19                 // Method java/lang/Integer.intValue:()I
       6: iconst_1
       7: if_icmpne     15
      10: iconst_1
      11: istore_0
      12: goto          81
      15: getstatic     #15                 // Field _NUM:Ljava/lang/Integer;
      18: invokevirtual #19                 // Method java/lang/Integer.intValue:()I
      21: iconst_3
      22: if_icmpne     30
      25: iconst_3
      26: istore_0
      27: goto          81
      30: getstatic     #15                 // Field _NUM:Ljava/lang/Integer;
      33: invokevirtual #19                 // Method java/lang/Integer.intValue:()I
      36: iconst_5
      37: if_icmpne     45
      40: iconst_5
      41: istore_0
      42: goto          81
      45: getstatic     #15                 // Field _NUM:Ljava/lang/Integer;
      48: invokevirtual #19                 // Method java/lang/Integer.intValue:()I
      51: bipush        7
      53: if_icmpne     62
      56: bipush        7
      58: istore_0
      59: goto          81
      62: getstatic     #15                 // Field _NUM:Ljava/lang/Integer;
      65: invokevirtual #19                 // Method java/lang/Integer.intValue:()I
      68: bipush        9
      70: if_icmpne     79
      73: bipush        9
      75: istore_0
      76: goto          81
      79: iconst_m1
      80: istore_0
      81: return

  static {};
    Code:
       0: iconst_1
       1: 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(); // 執行測試
    }

    @Benchmark
    public 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;
        }
    }

    @Benchmark
    public 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_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: invokestatic  #7                  // Method tableSwitchTest:()V
       3: invokestatic  #12                 // Method lookupSwitchTest:()V
       6: return

  public static void tableSwitchTest();
    Code:
       0: getstatic     #15                 // Field _NUM:Ljava/lang/Integer;
       3: invokevirtual #19                 // Method java/lang/Integer.intValue:()I
       6: tableswitch   { // 1 to 9
                     1: 56
                     2: 61
                     3: 66
                     4: 71
                     5: 76
                     6: 81
                     7: 87
                     8: 93
                     9: 99
               default: 105
          }
      56: iconst_1
      57: istore_0
      58: goto          107
      61: iconst_2
      62: istore_0
      63: goto          107
      66: iconst_3
      67: istore_0
      68: goto          107
      71: iconst_4
      72: istore_0
      73: goto          107
      76: iconst_5
      77: istore_0
      78: goto          107
      81: bipush        6
      83: istore_0
      84: goto          107
      87: bipush        7
      89: istore_0
      90: goto          107
      93: bipush        8
      95: istore_0
      96: goto          107
      99: bipush        9
     101: istore_0
     102: goto          107
     105: iconst_m1
     106: istore_0
     107: return

  public static void lookupSwitchTest();
    Code:
       0: getstatic     #15                 // Field _NUM:Ljava/lang/Integer;
       3: invokevirtual #19                 // Method java/lang/Integer.intValue:()I
       6: lookupswitch  { // 9
                     1: 88
                     3: 98
                     4: 103
                     6: 113
                     8: 125
                    11: 93
                    19: 108
                    33: 119
                   999: 131
               default: 137
          }
      88: iconst_1
      89: istore_0
      90: goto          139
      93: iconst_2
      94: istore_0
      95: goto          139
      98: iconst_3
      99: istore_0
     100: goto          139
     103: iconst_4
     104: istore_0
     105: goto          139
     108: iconst_5
     109: istore_0
     110: goto          139
     113: bipush        6
     115: istore_0
     116: goto          139
     119: bipush        7
     121: istore_0
     122: goto          139
     125: bipush        8
     127: istore_0
     128: goto          139
     131: bipush        9
     133: istore_0
     134: goto          139
     137: iconst_m1
     138: istore_0
     139: return

  static {};
    Code:
       0: iconst_1
       1: 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(); // 執行測試
    }

    @Benchmark
    public 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;
        }
    }

    @Benchmark
    public 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 的性能要高

關鍵字: