Mengapa beralih lebih cepat daripada jika

116

Banyak buku Java yang menggambarkan switchpernyataan itu lebih cepat daripada if elsepernyataan itu. Tetapi saya tidak menemukan di mana pun mengapa peralihan lebih cepat daripada jika .

Contoh

Saya memiliki situasi saya harus memilih satu dari dua item. Saya bisa menggunakan keduanya

switch (item) {
    case BREAD:
        //eat Bread
        break;
    default:
        //leave the restaurant
}

atau

if (item == BREAD) {
    //eat Bread
} else {
    //leave the restaurant
}

mempertimbangkan item dan ROTI adalah nilai int konstan.

Dalam contoh di atas, mana yang lebih cepat beraksi dan mengapa?

Jacob van Lingen
sumber
Mungkin ini juga jawaban untuk java: stackoverflow.com/questions/767821/…
Tobias
19
Secara umum, dari Wikipedia : Jika rentang nilai input diidentifikasi 'kecil' dan hanya memiliki sedikit celah, beberapa kompiler yang menggabungkan pengoptimal sebenarnya dapat mengimplementasikan pernyataan switch sebagai tabel cabang atau array penunjuk fungsi yang diindeks alih-alih a serangkaian instruksi bersyarat yang panjang. Hal ini memungkinkan pernyataan switch untuk menentukan dengan cepat cabang apa yang akan dijalankan tanpa harus melalui daftar perbandingan.
Felix Kling
Jawaban teratas untuk pertanyaan ini menjelaskannya dengan cukup baik. Artikel ini juga menjelaskan semuanya dengan cukup baik.
bezmax
Saya berharap bahwa dalam banyak keadaan, kompilator yang mengoptimalkan akan dapat menghasilkan kode yang memiliki karakteristik kinerja yang serupa. Bagaimanapun, Anda harus menelepon jutaan kali untuk melihat perbedaan apa pun.
Mitch Wheat
2
Anda harus waspada terhadap buku yang membuat pernyataan seperti ini tanpa penjelasan / bukti / alasan.
matt b

Jawaban:

110

Karena ada bytecode khusus yang memungkinkan evaluasi pernyataan saklar efisien ketika ada banyak kasus.

Jika diimplementasikan dengan pernyataan IF Anda akan memiliki cek, lompatan ke klausa berikutnya, cek, lompat ke klausa berikutnya dan seterusnya. Dengan sakelar, JVM memuat nilai untuk membandingkan dan mengulangi tabel nilai untuk menemukan kecocokan, yang lebih cepat dalam banyak kasus.

Daniel
sumber
6
Bukankah iterasi diterjemahkan menjadi "check, jump"?
fivetwentysix
17
@fivetwentysix: Tidak, lihat ini untuk info: artima.com/underthehood/flowP.html . Kutipan dari artikel: Ketika JVM menemukan instruksi tableswitch, itu hanya dapat memeriksa untuk melihat apakah kuncinya berada dalam kisaran yang ditentukan oleh rendah dan tinggi. Jika tidak, dibutuhkan offset cabang default. Jika demikian, itu hanya mengurangi rendah dari kunci untuk mendapatkan offset ke dalam daftar offset cabang. Dengan cara ini, ia dapat menentukan offset cabang yang sesuai tanpa harus memeriksa setiap nilai kasus.
bezmax
1
(i) a switchtidak boleh diterjemahkan ke dalam tableswitchinstruksi bytecode - ini bisa menjadi lookupswitchinstruksi yang bekerja serupa dengan if / else (ii) bahkan tableswitchinstruksi bytecode dapat dikompilasi menjadi serangkaian if / else oleh JIT, tergantung pada faktor seperti jumlah cases.
assylias
1
tableswitchvs loopuswitch: stackoverflow.com/questions/10287700/…
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
34

Sebuah switchpernyataan tidak selalu lebih cepat daripada ifpernyataan. Ini berskala lebih baik daripada daftar panjang if-elsepernyataan karena switchdapat melakukan pencarian berdasarkan semua nilai. Namun, untuk kondisi yang singkat tidak akan lebih cepat dan bisa lebih lambat.

Peter Lawrey
sumber
5
Harap batasi "panjang". Lebih dari 5? Lebih dari 10? atau lebih seperti 20 - 30?
vanderwyst
11
Saya menduga itu tergantung. Bagi saya, 3 saran atau lebih switchakan lebih jelas jika tidak lebih cepat.
Peter Lawrey
Dalam kondisi apa ini bisa lebih lambat?
Eric
1
@Eric itu lebih lambat untuk sejumlah kecil nilai esp String atau int yang jarang.
Peter Lawrey
8

JVM saat ini memiliki dua jenis kode byte switch: LookupSwitch dan TableSwitch.

Setiap kasus dalam pernyataan switch memiliki offset integer, jika offset ini berdekatan (atau sebagian besar bersebelahan tanpa celah besar) (kasus 0: kasus 1: kasus 2, dll.), Maka TableSwitch digunakan.

Jika offset tersebar dengan celah yang besar (kasus 0: kasus 400: kasus 93748 :, dll.), Maka LookupSwitch digunakan.

Singkatnya, perbedaannya adalah bahwa TableSwitch dilakukan dalam waktu yang konstan karena setiap nilai dalam rentang nilai yang mungkin diberikan offset kode byte tertentu. Jadi, saat Anda memberikan pernyataan offset 3, ia tahu untuk melompat ke depan 3 untuk menemukan cabang yang benar.

Switch pencarian menggunakan pencarian biner untuk menemukan cabang kode yang benar. Ini berjalan dalam waktu O (log n), yang masih bagus, tapi bukan yang terbaik.

Untuk informasi lebih lanjut tentang ini, lihat di sini: Perbedaan antara JVM LookupSwitch dan TableSwitch?

Sejauh mana yang tercepat, gunakan pendekatan ini: Jika Anda memiliki 3 kasus atau lebih yang nilainya berurutan atau hampir berurutan, selalu gunakan sakelar.

Jika Anda memiliki 2 kasus, gunakan pernyataan if.

Untuk situasi lain, peralihan kemungkinan besar lebih cepat, tetapi itu tidak dijamin, karena pencarian biner di LookupSwitch dapat mencapai skenario yang buruk.

Juga, perlu diingat bahwa JVM akan menjalankan pengoptimalan JIT pada pernyataan if yang akan mencoba menempatkan cabang terpanas terlebih dahulu dalam kode. Ini disebut "Prediksi Cabang". Untuk informasi lebih lanjut tentang ini, lihat di sini: https://dzone.com/articles/branch-prediction-in-java

Pengalaman Anda mungkin berbeda. Saya tidak tahu bahwa JVM tidak menjalankan pengoptimalan serupa di LookupSwitch, tetapi saya telah belajar untuk mempercayai pengoptimalan JIT dan tidak mencoba mengakali kompiler.

HesNotTheStig
sumber
1
Sejak memposting ini, telah menjadi perhatian saya bahwa "ganti ekspresi" dan "pencocokan pola" akan hadir di Java, mungkin segera setelah Java 12. openjdk.java.net/jeps/325 openjdk.java.net/jeps/305 Belum ada yang konkret, tetapi tampaknya ini akan membuat switchfitur bahasa yang lebih kuat. Pencocokan pola, misalnya, akan memungkinkan pencarian yang lebih lancar dan berperforma instanceof. Namun, menurut saya aman untuk mengasumsikan bahwa untuk skenario switch / if dasar, aturan yang saya sebutkan akan tetap berlaku.
HesNotTheStig
1

Jadi jika Anda berencana untuk memiliki banyak paket memori tidak terlalu mahal hari ini dan arraynya cukup cepat. Anda juga tidak dapat mengandalkan pernyataan switch untuk membuat tabel lompat secara otomatis sehingga lebih mudah untuk membuat sendiri skenario tabel lompat. Seperti yang Anda lihat pada contoh di bawah ini, kami mengasumsikan maksimal 255 paket.

Untuk mendapatkan hasil di bawah ini Anda perlu abstraksi .. saya tidak akan menjelaskan cara kerjanya jadi semoga Anda memiliki pemahaman tentang itu.

Saya memperbarui ini untuk mengatur ukuran paket menjadi 255 jika Anda membutuhkan lebih dari itu Anda harus melakukan pemeriksaan batas untuk (id <0) || (id> length).

Packets[] packets = new Packets[255];

static {
     packets[0] = new Login(6);
     packets[2] = new Logout(8);
     packets[4] = new GetMessage(1);
     packets[8] = new AddFriend(0);
     packets[11] = new JoinGroupChat(7); // etc... not going to finish.
}

public void handlePacket(IncomingData data)
{
    int id = data.readByte() & 0xFF; //Secure value to 0-255.

    if (packet[id] == null)
        return; //Leave if packet is unhandled.

    packets[id].execute(data);
}

Edit karena saya sering menggunakan Jump Table di C ++ sekarang saya akan menunjukkan contoh tabel lompat fungsi pointer. Ini adalah contoh yang sangat umum, tetapi saya telah menjalankannya dan berfungsi dengan benar. Perlu diingat Anda harus mengatur pointer ke NULL, C ++ tidak akan melakukan ini secara otomatis seperti di Java.

#include <iostream>

struct Packet
{
    void(*execute)() = NULL;
};

Packet incoming_packet[255];
uint8_t test_value = 0;

void A() 
{ 
    std::cout << "I'm the 1st test.\n";
}

void B() 
{ 
    std::cout << "I'm the 2nd test.\n";
}

void Empty() 
{ 

}

void Update()
{
    if (incoming_packet[test_value].execute == NULL)
        return;

    incoming_packet[test_value].execute();
}

void InitializePackets()
{
    incoming_packet[0].execute = A;
    incoming_packet[2].execute = B;
    incoming_packet[6].execute = A;
    incoming_packet[9].execute = Empty;
}

int main()
{
    InitializePackets();

    for (int i = 0; i < 512; ++i)
    {
        Update();
        ++test_value;
    }
    system("pause");
    return 0;
}

Hal lain yang ingin saya kemukakan adalah Divide and Conquer yang terkenal. Jadi ide array saya di atas 255 dapat dikurangi menjadi tidak lebih dari 8 pernyataan if sebagai skenario kasus terburuk.

Yaitu tetapi perlu diingat itu menjadi berantakan dan sulit untuk dikelola dengan cepat dan pendekatan saya yang lain umumnya lebih baik, tetapi ini digunakan dalam kasus di mana array tidak akan memotongnya. Anda harus mencari tahu kasus penggunaan Anda dan kapan setiap situasi bekerja paling baik. Sama seperti Anda tidak ingin menggunakan salah satu dari pendekatan ini jika Anda hanya memiliki sedikit pemeriksaan.

If (Value >= 128)
{
   if (Value >= 192)
   {
        if (Value >= 224)
        {
             if (Value >= 240)
             {
                  if (Value >= 248)
                  {
                      if (Value >= 252)
                      {
                          if (Value >= 254)
                          {
                              if (value == 255)
                              {

                              } else {

                              }
                          }
                      }
                  }
             }      
        }
   }
}
Jeremy Trifilo
sumber
2
Mengapa tipuan ganda? Karena ID harus dibatasi, mengapa tidak hanya memeriksa batas-batas id yang masuk sebagai 0 <= id < packets.lengthdan memastikan packets[id]!=nulldan kemudian melakukannya packets[id].execute(data)?
Lawrence Dol
ya maaf untuk respon terlambat melihat ini lagi .. dan saya tidak tahu apa yang saya pikirkan saya memperbarui posting lol dan membatasi paket ke ukuran byte unsigned sehingga tidak perlu pemeriksaan panjang.
Jeremy Trifilo
0

Pada tingkat bytecode, variabel subjek dimuat hanya sekali ke register prosesor dari alamat memori dalam file .class terstruktur yang dimuat oleh Runtime, dan ini ada dalam pernyataan switch; sedangkan dalam pernyataan-if, instruksi jvm yang berbeda dihasilkan oleh DE kompilasi kode Anda, dan ini mengharuskan setiap variabel dimuat ke dalam register meskipun variabel yang sama digunakan seperti pada pernyataan-if berikutnya. Jika Anda tahu tentang pengkodean dalam bahasa assembly maka ini akan menjadi hal biasa; meskipun java compiled cox bukan bytecode, atau kode mesin langsung, konsep kondisionalnya masih konsisten. Nah, saya mencoba menghindari teknis yang lebih dalam saat menjelaskan. Saya harap saya telah membuat konsep itu jelas dan tersingkap. Terima kasih.

Ayat Villalon Gamboa
sumber