Apa perbedaan kinerja relatif pernyataan if / else versus switch di Java?

123

Khawatir tentang performa aplikasi web saya, saya bertanya-tanya pernyataan "if / else" atau switch mana yang lebih baik terkait performa?

Anth0
sumber
6
Apakah Anda punya alasan untuk berpikir bytecode yang sama tidak dibuat untuk dua konstruksi?
Pascal Cuoq
2
@Pascal: mungkin ada pengoptimalan yang dilakukan dengan menggunakan pencarian tabel alih-alih daftar ifdll.
jldupont
18
"Optimalisasi dini adalah akar dari segala kejahatan" - Donald Knuth
missingfaktor
104
Meskipun ini jelas merupakan pengoptimalan yang prematur, "Kepatuhan yang tidak masuk akal terhadap kutipan yang diambil dengan buruk di luar konteks adalah alasan kami membutuhkan komputer multi-core kelas atas hanya untuk menampilkan GUI yang cukup responsif hari ini" - Saya.
Lawrence Dol
2
Knuth memiliki pikiran yang tepat. Harap perhatikan kualifikasi "prematur". Optimasi adalah perhatian yang sangat valid. Yang mengatakan, server terikat IO dan kemacetan jaringan dan disk I / O adalah lipat lebih signifikan daripada apa pun yang terjadi di server Anda.
alphazero

Jawaban:

108

Itu adalah pengoptimalan mikro dan pengoptimalan prematur, yang jahat. Agak khawatir tentang keterbacaan dan pemeliharaan kode yang dipermasalahkan. Jika ada lebih dari dua if/elsebalok yang direkatkan atau ukurannya tidak dapat diprediksi, maka Anda mungkin sangat mempertimbangkan switchpernyataan.

Atau, Anda juga bisa mengambil Polimorfisme . Pertama buat beberapa antarmuka:

public interface Action { 
    void execute(String input);
}

Dan dapatkan semua implementasi di beberapa Map. Anda dapat melakukan ini baik secara statis atau dinamis:

Map<String, Action> actions = new HashMap<String, Action>();

Terakhir, ganti if/elseatau switchdengan sesuatu seperti ini (dengan mengesampingkan pemeriksaan sepele seperti nullpointers):

actions.get(name).execute(input);

Ini mungkin menjadi microslower dari if/elseatau switch, tetapi kode ini setidaknya jauh lebih baik dipertahankan.

Saat Anda berbicara tentang aplikasi web, Anda dapat menggunakan HttpServletRequest#getPathInfo()sebagai kunci tindakan (akhirnya menulis beberapa kode lagi untuk memisahkan bagian terakhir dari pathinfo dalam satu lingkaran sampai tindakan ditemukan). Anda dapat menemukan jawaban serupa di sini:

Jika Anda mengkhawatirkan kinerja aplikasi web Java EE secara umum, artikel ini juga dapat bermanfaat bagi Anda. Ada area lain yang memberikan lebih banyak keuntungan kinerja daripada hanya (mikro) yang mengoptimalkan kode Java mentah.

BalusC
sumber
1
atau pertimbangkan polimorfisme sebagai gantinya
jk.
Itu memang lebih disarankan jika jumlah blok if / else "tidak dapat diprediksi".
BalusC
73
Saya tidak begitu cepat menganggap semua pengoptimalan awal sebagai "jahat". Menjadi terlalu agresif itu bodoh, tetapi ketika dihadapkan dengan konstruksi yang dapat dibaca sebanding memilih salah satu yang diketahui berkinerja lebih baik adalah keputusan yang tepat.
Brian Knoblauch
8
Versi pencarian HashMap dapat dengan mudah 10 kali lebih lambat dibandingkan dengan instruksi tableswitsch. Saya tidak akan menyebutnya "microslower"!
x4u
7
Saya tertarik untuk benar-benar mengetahui cara kerja Java dalam kasus umum dengan pernyataan switch - Saya tidak tertarik apakah seseorang berpikir ini terkait dengan pengoptimalan awal yang terlalu memprioritaskan atau tidak. Karena itu, saya sama sekali tidak tahu mengapa jawaban ini banyak disukai dan mengapa itu adalah jawaban yang diterima ... ini tidak melakukan apa pun untuk menjawab pertanyaan awal.
searchengine27
125

Saya sangat setuju dengan pendapat bahwa pengoptimalan prematur adalah sesuatu yang harus dihindari.

Tetapi memang benar bahwa Java VM memiliki bytecode khusus yang dapat digunakan untuk switch ().

Lihat Spesifikasi WM ( lookupswitch dan tableswitch )

Jadi mungkin ada beberapa peningkatan kinerja, jika kode tersebut merupakan bagian dari grafik CPU kinerja.

Waverick
sumber
60
Saya bertanya-tanya mengapa komentar ini tidak dinilai lebih tinggi: ini yang paling informitif dari semuanya. Maksud saya: kita semua sudah tahu tentang optimalisasi dini yang buruk dan semacamnya, tidak perlu dijelaskan untuk yang ke-1000 kalinya.
Folkert van Heusden
5
+1 Pada stackoverflow.com/a/15621602/89818 tampaknya peningkatan kinerja benar-benar ada, dan Anda akan melihat keuntungan jika menggunakan 18+ casing.
gak
52

Sangat tidak mungkin bahwa jika / lain atau sakelar akan menjadi sumber penurunan kinerja Anda. Jika Anda mengalami masalah kinerja, Anda harus melakukan analisis profil kinerja terlebih dahulu untuk menentukan di mana titik lambatnya. Optimasi prematur adalah akar dari segala kejahatan!

Namun demikian, dimungkinkan untuk membicarakan kinerja relatif switch vs. if / else dengan pengoptimalan kompilator Java. Catatan pertama bahwa di Java, pernyataan switch beroperasi pada domain yang sangat terbatas - integer. Secara umum, Anda dapat melihat pernyataan switch sebagai berikut:

switch (<condition>) {
   case c_0: ...
   case c_1: ...
   ...
   case c_n: ...
   default: ...
}

dimana c_0 ,, c_1..., dan c_Nadalah bilangan integral yang merupakan target pernyataan switch, dan <condition>harus menyelesaikan ekspresi integer.

  • Jika set ini "padat" - yaitu, (maks (c i ) + 1 - min (c i )) / n> α, di mana 0 <k <α <1, di mana klebih besar dari beberapa nilai empiris, a tabel lompat dapat dibuat, yang sangat efisien.

  • Jika himpunan ini tidak terlalu padat, tetapi n> = β, pohon pencarian biner dapat menemukan target dalam O (2 * log (n)) yang juga masih efisien.

Untuk semua kasus lainnya, pernyataan switch sama efisiennya dengan rangkaian pernyataan if / else yang setara. Nilai yang tepat dari α dan β bergantung pada sejumlah faktor dan ditentukan oleh modul pengoptimalan kode penyusun.

Akhirnya, tentu saja, jika domain dari <condition>bukan bilangan bulat, pernyataan switch sama sekali tidak berguna.

John Feminella
sumber
+1. Ada kemungkinan besar waktu yang dihabiskan untuk jaringan I / O dengan mudah menutupi masalah khusus ini.
Adam Paynter
3
Perlu dicatat bahwa switch bekerja dengan lebih dari sekedar int. Dari Tutorial Java: "Sebuah switch bekerja dengan tipe data primitif byte, short, char, dan int. Ia juga bekerja dengan tipe enumerasi (dibahas dalam Enum Type), kelas String, dan beberapa kelas khusus yang membungkus tipe primitif tertentu : Karakter, Byte, Short, dan Integer (dibahas dalam Bilangan dan String). " Dukungan untuk String adalah tambahan yang lebih baru; ditambahkan di Java 7. docs.oracle.com/javase/tutorial/java/nutsandbolts/switch.html
atraudes
1
@jhonFeminella Bisakah Anda membandingkan efek gagasan BIG O untuk String Java7 di Swtich dibandingkan dengan String di if / else if ..?
Kanagavelu Sugumar
Lebih tepatnya, javac 8 memberikan bobot 3 untuk kompleksitas waktu daripada kompleksitas ruang: stackoverflow.com/a/31032054/895245
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
11

Gunakan saklar!

Saya benci mempertahankan blok if-else-! Lakukan tes:

public class SpeedTestSwitch
{
    private static void do1(int loop)
    {
        int temp = 0;
        for (; loop > 0; --loop)
        {
            int r = (int) (Math.random() * 10);
            switch (r)
            {
                case 0:
                    temp = 9;
                    break;
                case 1:
                    temp = 8;
                    break;
                case 2:
                    temp = 7;
                    break;
                case 3:
                    temp = 6;
                    break;
                case 4:
                    temp = 5;
                    break;
                case 5:
                    temp = 4;
                    break;
                case 6:
                    temp = 3;
                    break;
                case 7:
                    temp = 2;
                    break;
                case 8:
                    temp = 1;
                    break;
                case 9:
                    temp = 0;
                    break;
            }
        }
        System.out.println("ignore: " + temp);
    }

    private static void do2(int loop)
    {
        int temp = 0;
        for (; loop > 0; --loop)
        {
            int r = (int) (Math.random() * 10);
            if (r == 0)
                temp = 9;
            else
                if (r == 1)
                    temp = 8;
                else
                    if (r == 2)
                        temp = 7;
                    else
                        if (r == 3)
                            temp = 6;
                        else
                            if (r == 4)
                                temp = 5;
                            else
                                if (r == 5)
                                    temp = 4;
                                else
                                    if (r == 6)
                                        temp = 3;
                                    else
                                        if (r == 7)
                                            temp = 2;
                                        else
                                            if (r == 8)
                                                temp = 1;
                                            else
                                                if (r == 9)
                                                    temp = 0;
        }
        System.out.println("ignore: " + temp);
    }

    public static void main(String[] args)
    {
        long time;
        int loop = 1 * 100 * 1000 * 1000;
        System.out.println("warming up...");
        do1(loop / 100);
        do2(loop / 100);

        System.out.println("start");

        // run 1
        System.out.println("switch:");
        time = System.currentTimeMillis();
        do1(loop);
        System.out.println(" -> time needed: " + (System.currentTimeMillis() - time));

        // run 2
        System.out.println("if/else:");
        time = System.currentTimeMillis();
        do2(loop);
        System.out.println(" -> time needed: " + (System.currentTimeMillis() - time));
    }
}

Kode standar C # saya untuk pembandingan

Bitterblue
sumber
Bisakah Anda (kadang-kadang) menjelaskan sedikit tentang bagaimana Anda melakukan benchmark ini?
DerMike
Terima kasih banyak atas pembaruan Anda. Maksud saya, mereka berbeda dalam satu urutan besarnya - yang mungkin saja. Apakah Anda yakin bahwa kompilator tidak hanya mengoptimalkan switches itu?
DerMike
@DerMike Saya tidak ingat bagaimana saya mendapatkan hasil lama. Saya menjadi sangat berbeda hari ini. Tapi coba sendiri dan beri tahu saya bagaimana hasilnya.
Bitterblue
1
ketika saya menjalankannya di laptop saya; ganti waktu yang dibutuhkan: 3585, jika / lain waktu yang dibutuhkan: 3458 jadi jika / lain lebih baik :) atau tidak lebih buruk
halil
1
Biaya dominan dalam pengujian adalah pembuatan bilangan acak. Saya memodifikasi tes untuk menghasilkan nomor acak sebelum loop, dan menggunakan nilai temp untuk memberi makan kembali ke r. Saklar kemudian hampir dua kali lebih cepat dari rantai if-else.
boneill
8

Saya ingat pernah membaca bahwa ada 2 jenis pernyataan Switch di bytecode Java. (Saya pikir itu di 'Java Performance Tuning' One adalah implementasi yang sangat cepat yang menggunakan nilai integer pernyataan switch untuk mengetahui offset kode yang akan dieksekusi. Ini akan membutuhkan semua integer untuk berurutan dan dalam kisaran yang terdefinisi dengan baik Saya menduga bahwa menggunakan semua nilai Enum akan termasuk dalam kategori itu juga.

Saya setuju dengan banyak poster lain ... mungkin terlalu dini untuk mengkhawatirkan hal ini, kecuali ini adalah kode yang sangat panas.

malaverdiere
sumber
4
1 untuk komentar kode panas. Jika itu di loop utama Anda, itu tidak prematur.
KingAndrew
Ya, javac menerapkan switchbeberapa cara berbeda, beberapa lebih efisien daripada yang lain. Secara umum, efisiensinya tidak lebih buruk daripada " iftangga" lurus ke depan , tetapi ada cukup banyak variasi (terutama dengan JITC) sehingga sulit untuk lebih tepat dari itu.
Hot Licks
8

Menurut Cliff Click dalam bukunya Java One talk 2009 A Crash Course in Modern Hardware :

Saat ini, performa didominasi oleh pola akses memori. Cache merindukan mendominasi - memori adalah disk baru. [Slide 65]

Anda bisa mendapatkan slide lengkapnya di sini .

Cliff memberi contoh (menyelesaikan Slide 30) yang menunjukkan bahwa meskipun CPU melakukan penggantian nama register, prediksi cabang, dan eksekusi spekulatif, ia hanya dapat memulai 7 operasi dalam 4 siklus jam sebelum harus memblokir karena dua cache yang meleset. 300 siklus jam untuk kembali.

Jadi dia mengatakan untuk mempercepat program Anda, Anda seharusnya tidak melihat masalah kecil seperti ini, tetapi pada masalah yang lebih besar seperti apakah Anda membuat konversi format data yang tidak perlu, seperti mengonversi "SOAP → XML → DOM → SQL →… "yang" melewatkan semua data melalui cache ".

Jim Ferrans
sumber
4

Dalam pengujian saya, kinerja yang lebih baik adalah ENUM> MAP> SWITCH> IF / ELSE IF di Windows7.

import java.util.HashMap;
import java.util.Map;

public class StringsInSwitch {
public static void main(String[] args) {
    String doSomething = null;


    //METHOD_1 : SWITCH
    long start = System.currentTimeMillis();
    for (int i = 0; i < 99999999; i++) {
        String input = "Hello World" + (i & 0xF);

        switch (input) {
        case "Hello World0":
            doSomething = "Hello World0";
            break;
        case "Hello World1":
            doSomething = "Hello World0";
            break;
        case "Hello World2":
            doSomething = "Hello World0";
            break;
        case "Hello World3":
            doSomething = "Hello World0";
            break;
        case "Hello World4":
            doSomething = "Hello World0";
            break;
        case "Hello World5":
            doSomething = "Hello World0";
            break;
        case "Hello World6":
            doSomething = "Hello World0";
            break;
        case "Hello World7":
            doSomething = "Hello World0";
            break;
        case "Hello World8":
            doSomething = "Hello World0";
            break;
        case "Hello World9":
            doSomething = "Hello World0";
            break;
        case "Hello World10":
            doSomething = "Hello World0";
            break;
        case "Hello World11":
            doSomething = "Hello World0";
            break;
        case "Hello World12":
            doSomething = "Hello World0";
            break;
        case "Hello World13":
            doSomething = "Hello World0";
            break;
        case "Hello World14":
            doSomething = "Hello World0";
            break;
        case "Hello World15":
            doSomething = "Hello World0";
            break;
        }
    }

    System.out.println("Time taken for String in Switch :"+ (System.currentTimeMillis() - start));




    //METHOD_2 : IF/ELSE IF
    start = System.currentTimeMillis();

    for (int i = 0; i < 99999999; i++) {
        String input = "Hello World" + (i & 0xF);

        if(input.equals("Hello World0")){
            doSomething = "Hello World0";
        } else if(input.equals("Hello World1")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World2")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World3")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World4")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World5")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World6")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World7")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World8")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World9")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World10")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World11")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World12")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World13")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World14")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World15")){
            doSomething = "Hello World0";

        }
    }
    System.out.println("Time taken for String in if/else if :"+ (System.currentTimeMillis() - start));









    //METHOD_3 : MAP
    //Create and build Map
    Map<String, ExecutableClass> map = new HashMap<String, ExecutableClass>();
    for (int i = 0; i <= 15; i++) {
        String input = "Hello World" + (i & 0xF);
        map.put(input, new ExecutableClass(){
                            public void execute(String doSomething){
                                doSomething = "Hello World0";
                            }
                        });
    }


    //Start test map
    start = System.currentTimeMillis();
    for (int i = 0; i < 99999999; i++) {
        String input = "Hello World" + (i & 0xF);
        map.get(input).execute(doSomething);
    }
    System.out.println("Time taken for String in Map :"+ (System.currentTimeMillis() - start));






    //METHOD_4 : ENUM (This doesn't use muliple string with space.)
    start = System.currentTimeMillis();
    for (int i = 0; i < 99999999; i++) {
        String input = "HW" + (i & 0xF);
        HelloWorld.valueOf(input).execute(doSomething);
    }
    System.out.println("Time taken for String in ENUM :"+ (System.currentTimeMillis() - start));


    }

}

interface ExecutableClass
{
    public void execute(String doSomething);
}



// Enum version
enum HelloWorld {
    HW0("Hello World0"), HW1("Hello World1"), HW2("Hello World2"), HW3(
            "Hello World3"), HW4("Hello World4"), HW5("Hello World5"), HW6(
            "Hello World6"), HW7("Hello World7"), HW8("Hello World8"), HW9(
            "Hello World9"), HW10("Hello World10"), HW11("Hello World11"), HW12(
            "Hello World12"), HW13("Hello World13"), HW14("Hello World4"), HW15(
            "Hello World15");

    private String name = null;

    private HelloWorld(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void execute(String doSomething){
        doSomething = "Hello World0";
    }

    public static HelloWorld fromString(String input) {
        for (HelloWorld hw : HelloWorld.values()) {
            if (input.equals(hw.getName())) {
                return hw;
            }
        }
        return null;
    }

}





//Enum version for betterment on coding format compare to interface ExecutableClass
enum HelloWorld1 {
    HW0("Hello World0") {   
        public void execute(String doSomething){
            doSomething = "Hello World0";
        }
    }, 
    HW1("Hello World1"){    
        public void execute(String doSomething){
            doSomething = "Hello World0";
        }
    };
    private String name = null;

    private HelloWorld1(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void execute(String doSomething){
    //  super call, nothing here
    }
}


/*
 * http://stackoverflow.com/questions/338206/why-cant-i-switch-on-a-string
 * https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-3.html#jvms-3.10
 * http://forums.xkcd.com/viewtopic.php?f=11&t=33524
 */ 
Kanagavelu Sugumar
sumber
Time taken for String in Switch :3235 Time taken for String in if/else if :3143 Time taken for String in Map :4194 Time taken for String in ENUM :2866
halil
@halil Saya tidak yakin bagaimana kode ini bekerja pada lingkungan yang berbeda, tetapi Anda telah menyebutkan jika / elseif lebih baik daripada Switch dan Peta, bahwa saya tidak dapat meyakinkan karena jika / elseif harus melakukan lebih tidak kali sama dengan perbandingan.
Kanagavelu Sugumar
2

Untuk sebagian besar switchdan sebagian besar if-then-elseblok, saya tidak dapat membayangkan bahwa ada masalah terkait kinerja yang cukup berarti atau signifikan.

Tapi inilah masalahnya: jika Anda menggunakan switchblok, penggunaannya menunjukkan bahwa Anda mengaktifkan nilai yang diambil dari sekumpulan konstanta yang diketahui pada waktu kompilasi. Dalam kasus ini, Anda seharusnya tidak menggunakan switchpernyataan sama sekali jika Anda dapat menggunakanenum dengan konstanta spesifik.

Dibandingkan dengan switchpernyataan, enum memberikan keamanan tipe dan kode yang lebih baik yang lebih mudah dipelihara. Enum dapat dirancang sehingga jika sebuah konstanta ditambahkan ke himpunan konstanta, kode Anda tidak akan dikompilasi tanpa menyediakan metode spesifik-konstanta untuk nilai baru. Di sisi lain, lupa menambahkan yang baru caseke sebuah switchblok terkadang hanya dapat ditangkap pada waktu proses jika Anda cukup beruntung untuk mengatur blok Anda untuk membuat pengecualian.

Performa antara switchdan enummetode spesifik-konstan seharusnya tidak jauh berbeda, tetapi metode terakhir lebih mudah dibaca, lebih aman, dan lebih mudah dikelola.

scottb
sumber