switch 的性能提升了 3 倍,我只用了這一招!

這是我的第 190 期分享

作者 | 王磊

來源 | Java中文社羣(ID:javacn666) 

分享 | Java中文社羣(ID:javacn666)

上一篇《if快還是switch快?解密switch背後的祕密》我們測試了 if 和 switch 的性能,得出了要儘量使用 switch 的結論,因爲他的效率比 if 高很多,具體原因點擊上文連接查看。

既然 switch 如此有魅力,那麼有沒有更好的方法,讓 switch 變得更快一些呢

答案是有的,不然本文就不會誕生了不是?

在上篇 if 和 switch 性能對比的文章中有讀者問到:String 類型的 switch 性能是否也比 if 高?先說答案,String 類型的條件判斷 switch 的性能依舊比 if 好

口說無憑,先舉個????,測試代碼如下:

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;
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 = 3, timeUnit = TimeUnit.SECONDS) // 測試 5 輪,每次 3s
@Fork(1) // fork 1 個線程
@State(Scope.Thread) // 每個測試線程一個實例
public class SwitchOptimizeByStringTest {
    static String _STR = "Java中文社羣";
    public static void main(String[] args) throws RunnerException {
        // 啓動基準測試
        Options opt = new OptionsBuilder()
                .include(SwitchOptimizeByStringTest.class.getSimpleName()) // 要導入的測試類
                .build();
        new Runner(opt).run(); // 執行測試
    }

    @Benchmark
    public void switchTest(Blackhole blackhole) {
        String s1;
        switch (_STR) {
            case "java":
                s1 = "java";
                break;
            case "mysql":
                s1 = "mysql";
                break;
            case "oracle":
                s1 = "oracle";
                break;
            case "redis":
                s1 = "redis";
                break;
            case "mq":
                s1 = "mq";
                break;
            case "kafka":
                s1 = "kafka";
                break;
            case "rabbitmq":
                s1 = "rabbitmq";
                break;
            default:
                s1 = "default";
                break;
        }
        // 爲了避免 JIT 忽略未被使用的結果計算,可以使用 Blackhole#consume 來保證方法被正常執行
        blackhole.consume(s1);
    }

    @Benchmark
    public void ifTest(Blackhole blackhole) {
        String s1;
        if ("java".equals(_STR)) {
            s1 = "java";
        } else if ("mysql".equals(_STR)) {
            s1 = "mysql";
        } else if ("oracle".equals(_STR)) {
            s1 = "oracle";
        } else if ("redis".equals(_STR)) {
            s1 = "redis";
        } else if ("mq".equals(_STR)) {
            s1 = "mq";
        } else if ("kafka".equals(_STR)) {
            s1 = "kafka";
        } else if ("rabbitmq".equals(_STR)) {
            s1 = "rabbitmq";
        } else {
            s1 = "default";
        }
        // 爲了避免 JIT 忽略未被使用的結果計算,可以使用 Blackhole#consume 來保證方法被正常執行
        blackhole.consume(s1);
    }
}

特殊說明:本文使用的是 Oracle 官方提供的性能測試工具 JMH(Java Microbenchmark Harness,JAVA 微基準測試套件)進行測試的。

以上代碼測試的結果如下:


從 Score 列(平均完成時間)可以看出 switch 的性能依舊比 if 的性能要高。

備註:本文的測試環境爲:JDK 1.8 / Mac mini (2018) / Idea 2020.1

switch 性能優化

我們知道在 JDK 1.7 之前 switch 是不支持 String 的,實際上 switch 只支持 int 類型

在 JDK 1.7 中的 String 類型,其實在編譯的時候會使用 hashCode 來作爲 switch 的實際值,以上 switch 判斷字符串的代碼,編譯爲字節碼實際結果如下:

public static void switchTest() {
    String var1 = _STR;
    byte var2 = -1;
    switch(var1.hashCode()) {
        case -1008861826:
            if (var1.equals("oracle")) {
                var2 = 2;
            }
            break;
        case -95168706:
            if (var1.equals("rabbitmq")) {
                var2 = 6;
            }
            break;
        case 3492:
            if (var1.equals("mq")) {
                var2 = 4;
            }
            break;
        case 3254818:
            if (var1.equals("java")) {
                var2 = 0;
            }
            break;
        case 101807910:
            if (var1.equals("kafka")) {
                var2 = 5;
            }
            break;
        case 104382626:
            if (var1.equals("mysql")) {
                var2 = 1;
            }
            break;
        case 108389755:
            if (var1.equals("redis")) {
                var2 = 3;
            }
    }
    // 忽略其他代碼...
}

知道了 switch 實現的本質,那麼優化就變得比較簡單了。

從以上的字節碼可以看出,如果要優化 switch 只需要把 String 類型變成 int 類型就可以了,這樣就剩了每個 case 中進行 if 判斷的性能消耗,最終的優化代碼如下:

public void switchHashCodeTest() {
    String s1;
    switch (_STR.hashCode()) {
        case 3254818:
            s1 = "java";
            break;
        case 104382626:
            s1 = "mysql";
            break;
        case -1008861826:
            s1 = "oracle";
            break;
        case 108389755:
            s1 = "redis";
            break;
        case 3492:
            s1 = "mq";
            break;
        case 101807910:
            s1 = "kafka";
            break;
        case -95168706:
            s1 = "rabbitmq";
            break;
        default:
            s1 = "default";
            break;
    }
}

此時我們使用 JMH 進行實際的測試,測試代碼如下:

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;
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 = 3, timeUnit = TimeUnit.SECONDS) // 測試 5 輪,每次 3s
@Fork(1) // fork 1 個線程
@State(Scope.Thread) // 每個測試線程一個實例
public class SwitchOptimizeByStringTest {
    static String _STR = "Java中文社羣";
    public static void main(String[] args) throws RunnerException {
        // 啓動基準測試
        Options opt = new OptionsBuilder()
                .include(SwitchOptimizeByStringTest.class.getSimpleName()) // 要導入的測試類
                .build();
        new Runner(opt).run(); // 執行測試
    }

    @Benchmark
    public void switchHashCodeTest(Blackhole blackhole) {
        String s1;
        switch (_STR.hashCode()) {
            case 3254818:
                s1 = "java";
                break;
            case 104382626:
                s1 = "mysql";
                break;
            case -1008861826:
                s1 = "oracle";
                break;
            case 108389755:
                s1 = "redis";
                break;
            case 3492:
                s1 = "mq";
                break;
            case 101807910:
                s1 = "kafka";
                break;
            case -95168706:
                s1 = "rabbitmq";
                break;
            default:
                s1 = "default";
                break;
        }
        // 爲了避免 JIT 忽略未被使用的結果計算,可以使用 Blackhole#consume 來保證方法被正常執行
        blackhole.consume(s1);
    }

    @Benchmark
    public void switchTest(Blackhole blackhole) {
        String s1;
        switch (_STR) {
            case "java":
                s1 = "java";
                break;
            case "mysql":
                s1 = "mysql";
                break;
            case "oracle":
                s1 = "oracle";
                break;
            case "redis":
                s1 = "redis";
                break;
            case "mq":
                s1 = "mq";
                break;
            case "kafka":
                s1 = "kafka";
                break;
            case "rabbitmq":
                s1 = "rabbitmq";
                break;
            default:
                s1 = "default";
                break;
        }
        // 爲了避免 JIT 忽略未被使用的結果計算,可以使用 Blackhole#consume 來保證方法被正常執行
        blackhole.consume(s1);
    }

    @Benchmark
    public void ifTest(Blackhole blackhole) {
        String s1;
        if ("java".equals(_STR)) {
            s1 = "java";
        } else if ("mysql".equals(_STR)) {
            s1 = "mysql";
        } else if ("oracle".equals(_STR)) {
            s1 = "oracle";
        } else if ("redis".equals(_STR)) {
            s1 = "redis";
        } else if ("mq".equals(_STR)) {
            s1 = "mq";
        } else if ("kafka".equals(_STR)) {
            s1 = "kafka";
        } else if ("rabbitmq".equals(_STR)) {
            s1 = "rabbitmq";
        } else {
            s1 = "default";
        }
        // 爲了避免 JIT 忽略未被使用的結果計算,可以使用 Blackhole#consume 來保證方法被正常執行
        blackhole.consume(s1);
    }
}

以上代碼測試的結果如下:


從以上結果可以看出,String 類型的 switch 判斷,經過優化之後,性能提升了 2.4 倍,可謂效果顯著。

注意事項

以上的 switch 優化是基於 String 類型的,同時我們需要注意 hashCode 重複的問題,例如對於字符串“Aa”和“BB”來說,他們的 hashCode 都是 2112,因此在優化是需要注意此類問題,也就是說我們使用 hashCode 時,必須保證判斷添加的值是已知的,並且最好不要出現 hashCode 重複的問題,如果出現此類問題,我們的解決方案是在 case 中進行判斷並賦值。

其他優化手段

我們本文重點討論的是 switch 性能優化的方案,當然如果處於性能考慮,我們還可以使用更加高效的替代方案,例如集合或者是枚舉,詳見我的另一篇文章《9個小技巧讓你的 if else看起來更優雅》

總結

通過本文我們知道 switch 本質上只支持 int 類型的條件判斷,即使是 JDK 1.7 中的 String 類型,最終編譯的時候還是會被轉化爲 hashCode(int)進行判斷。但因爲編譯成字節碼後會在 case 中使用 if equals 進行比較,所以性能並不算太高(只比 if 高一點點),因此我們可以直接把 String 轉化成 int 類型進行比較,從而避免在 case 中進行 if equals 判斷的性能消耗,這樣就大大的提升 switch 的性能,但需要注意的是,有些 key 值的 hashCode 是相同的,因此在優化時需要提前規避。

最後的話

原創不易,如果覺得本文對你有用,請隨手點擊一個「」,這是對作者最大的支持與鼓勵,謝謝你。

if快還是switch快?解密switch背後的祕密

HashMap 的 7 種遍歷方式與性能分析!「修正篇」

關注公衆號發送”進羣“,老王拉你進讀者羣。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章