用了這一招以後 switch 的性能提高了 3 倍!

上一篇《if快仍是switch快?解密switch背後的祕密》咱們測試了 if 和 switch 的性能,得出了要儘可能使用 switch 的結論,由於他的效率比 if 高不少,具體緣由點擊上文鏈接查看。java

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

答案是有的,否則本文就不會誕生了不是?redis

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

口說無憑,先舉個🌰,測試代碼以下:性能優化

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 微基準測試套件)進行測試的。bash

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

img

從 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);
    }
}
複製代碼

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

img

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

img

注意事項

以上的 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 是相同的,所以在優化時須要提早規避。

最後的話

原創不易,若是以爲本文對你有用,請隨手點擊一個「」,這是對做者最大的支持與鼓勵,謝謝你。

關注公衆號「Java中文社羣」回覆「乾貨」,獲取 50 篇原創乾貨 Top 榜

相關文章
相關標籤/搜索