Geçiş neden daha hızlıdır


116

Java kitaplarının switchçoğu ifadeyi ifadeden daha hızlı olarak tanımlar if else. Ama geçişin neden daha hızlı olduğunu hiçbir yerde bulamadım .

Misal

İkiden herhangi birini seçmem gereken bir durum var. Her ikisini de kullanabilirim

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

veya

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

item ve BREAD dikkate alındığında sabit bir int değeridir.

Yukarıdaki örnekte hangisi daha hızlıdır ve neden?


Belki bu, java için de bir cevaptır: stackoverflow.com/questions/767821/…
Tobias

19
Genel olarak, Wikipedia'dan : Girdi değerleri aralığı tanımlanabilir şekilde 'küçük' ise ve yalnızca birkaç boşluk varsa, bir optimize edici içeren bazı derleyiciler aslında switch ifadesini bir dal tablosu veya bir dizin yerine dizinlenmiş işlev işaretçileri dizisi olarak uygulayabilir. uzun şartlı talimatlar dizisi. Bu, switch deyiminin, bir karşılaştırma listesinden geçmek zorunda kalmadan, hangi dalın yürütüleceğini anında belirlemesini sağlar.
Felix Kling

Bu üst cevabı soruya oldukça iyi açıklıyor. Bu makale de her şeyi oldukça iyi açıklıyor.
bezmax

Çoğu durumda, optimize eden bir derleyicinin benzer performans özelliklerine sahip kodlar üretebileceğini umuyorum. Her durumda, herhangi bir farkı fark etmek için milyonlarca kez aramanız gerekir.
Mitch Wheat

2
Açıklama / kanıt / akıl yürütme olmadan bu tür ifadeler içeren kitaplara karşı dikkatli olmalısınız.
matt b

Yanıtlar:


110

Çünkü birçok durum olduğunda etkili anahtar deyimi değerlendirmesine izin veren özel bayt kodları vardır.

IF ifadeleri ile uygulanırsa, bir kontrol, sonraki cümleye atlama, kontrol, sonraki cümleye atlama vb. Olur. Anahtar ile JVM, karşılaştırılacak değeri yükler ve çoğu durumda daha hızlı olan bir eşleşme bulmak için değer tablosunda yinelenir.


6
Yineleme, "kontrol et, atla" anlamına gelmez mi?
fivetwentysix

17
@fivetwentysix: Hayır, bilgi için buna bakın: artima.com/underthehood/flowP.html . Makaleden alıntı: JVM bir tablo anahtarı talimatıyla karşılaştığında, anahtarın düşük ve yüksek olarak tanımlanan aralıkta olup olmadığını kontrol edebilir. Değilse, varsayılan dal ofsetini alır. Eğer öyleyse, dallanma ofsetleri listesine bir ofset almak için anahtardan düşük çıkarır. Bu şekilde, her durum değerini kontrol etmek zorunda kalmadan uygun dal ofsetini belirleyebilir.
bezmax

1
(i) a switch, bir tableswitchbayt kodu talimatına çevrilemez - lookupswitchif / else'e benzer şekilde çalışan bir talimat haline gelebilir (ii) bir tableswitchbayt kodu talimatları bile , faktörlere bağlı olarak JIT tarafından bir dizi if / else halinde derlenebilir. cases sayısı gibi .
asylias


34

Bir switchifade her zaman bir ififadeden daha hızlı değildir . Bu uzun bir listeden daha iyi scale if-elseolarak ifadeleri switchher değerlere dayalı bir arama yapabilirsiniz. Ancak, kısa bir koşul için daha hızlı olmayacak ve daha yavaş olabilir.


5
Lütfen "uzun" u sınırlayın. 5'ten büyük mü? 10'dan büyük mü? veya 20-30 gibi daha fazla?
vanderwyst

11
Değiştiğinden şüpheleniyorum. Benim için 3 veya daha fazla önerisi switch, daha hızlı değilse daha net olacaktır.
Peter Lawrey

Hangi koşullar altında daha yavaş olabilir?
Eric

1
@Eric, seyrek olan az sayıdaki değer için daha yavaştır, esp String veya int.
Peter Lawrey

8

Mevcut JVM'nin iki tür anahtar bayt kodu vardır: LookupSwitch ve TableSwitch.

Bir switch deyimindeki her durumda bir tamsayı uzaklığı vardır, eğer bu uzaklıklar bitişikse (veya büyük boşluklar olmadan çoğunlukla bitişikse) (durum 0: durum 1: durum 2, vb.), O zaman TableSwitch kullanılır.

Ofsetler büyük boşluklarla yayılırsa (durum 0: durum 400: durum 93748 :, vb.), LookupSwitch kullanılır.

Kısacası fark, TableSwitch'in sabit zamanda yapılmasıdır, çünkü olası değerler aralığındaki her bir değere belirli bir bayt kodu ofseti verilir. Böylece, ifadeye 3 ofset verdiğinizde, doğru dalı bulmak için 3'e atlamayı bilir.

Arama anahtarı, doğru kod dalını bulmak için ikili arama kullanır. Bu, O (log n) zamanında çalışır, bu hala iyidir, ancak en iyisi değildir.

Bununla ilgili daha fazla bilgi için buraya bakın: JVM'nin LookupSwitch ve TableSwitch arasındaki fark nedir?

Hangisinin en hızlı olduğu konusunda şu yaklaşımı kullanın: Değerleri ardışık veya neredeyse ardışık olan 3 veya daha fazla vakanız varsa, her zaman bir anahtar kullanın.

2 durumunuz varsa bir if ifadesi kullanın.

Diğer herhangi bir durum için, geçiş büyük olasılıkla daha hızlıdır, ancak LookupSwitch'teki ikili arama kötü bir senaryoya denk gelebileceğinden garanti edilmez.

Ayrıca, JVM'nin en sıcak dalı kodda ilk olarak yerleştirmeye çalışacak if ifadeleri üzerinde JIT optimizasyonlarını çalıştıracağını unutmayın. Buna "Dal Tahmini" denir. Bununla ilgili daha fazla bilgi için buraya bakın: https://dzone.com/articles/branch-prediction-in-java

Deneyimleriniz değişebilir. JVM'nin LookupSwitch üzerinde benzer bir optimizasyon çalıştırmadığını bilmiyorum, ancak JIT optimizasyonlarına güvenmeyi ve derleyiciyi alt etmeye çalışmamayı öğrendim.


1
Bunu yayınladığımdan beri, "ifadeleri değiştir" ve "kalıp eşleştirmesinin" Java'ya muhtemelen Java 12'den itibaren geldiğini fark ettim . Openjdk.java.net/jeps/325 openjdk.java.net/jeps/305 Henüz hiçbir şey somut değil, ancak bunların switchdaha da güçlü bir dil özelliği oluşturacağı görülüyor . Örneğin, kalıp eşleştirme, çok daha düzgün ve başarılı instanceofaramalara izin verecektir . Ancak, temel anahtar / senaryolar için bahsettiğim kuralın hala geçerli olacağını varsaymanın güvenli olduğunu düşünüyorum.
HesNotTheStig

1

Dolayısıyla, çok sayıda pakete sahip olmayı planlıyorsanız, bu günlerde bellek gerçekten büyük bir maliyet değil ve diziler oldukça hızlı. Ayrıca, bir atlama tablosunu otomatik olarak oluşturmak için bir switch ifadesine güvenemezsiniz ve bu nedenle, atlama tablosu senaryosunu kendiniz oluşturmak daha kolaydır. Aşağıdaki örnekte görebileceğiniz gibi, maksimum 255 paket varsayıyoruz.

Aşağıdaki sonucu elde etmek için ihtiyacınız olan soyutlama .. Bunun nasıl çalıştığını açıklamayacağım, umarım bunu anlarsınız.

Bunu, paket boyutunu 255'e ayarlamak için güncelledim, daha fazlasına ihtiyacınız varsa, daha sonra sınır kontrolü yapmanız gerekecek (id <0) || (id> uzunluk).

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);
}

Düzenleyin çünkü C ++ 'da bir Sıçrama Tablosu kullandığım için, şimdi bir fonksiyon işaretçisi atlama tablosu örneği göstereceğim. Bu çok genel bir örnek ama ben çalıştırdım ve düzgün çalışıyor. İşaretçiyi NULL olarak ayarlamanız gerektiğini unutmayın, C ++ bunu Java'daki gibi otomatik olarak yapmaz.

#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;
}

Ayrıca bahsetmek istediğim bir diğer nokta da ünlü Divide and Conquer. Dolayısıyla, 255'in üstündeki dizi fikrim, en kötü durum senaryosu olarak if ifadeleri 8'den fazla olmayacak şekilde azaltılabilir.

Yani, dağınık ve hızlı yönetmenin zor olduğunu ve diğer yaklaşımımın genellikle daha iyi olduğunu unutmayın, ancak bu, dizilerin kesmediği durumlarda kullanılır. Kullanım durumunuzu ve her durumun ne zaman en iyi şekilde çalıştığını belirlemelisiniz. Tıpkı birkaç kontrolünüz varsa bu yaklaşımlardan herhangi birini kullanmak istemeyeceğiniz gibi.

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 {

                              }
                          }
                      }
                  }
             }      
        }
   }
}

2
Neden çifte indirekt? Kimliğin yine de kısıtlanması gerektiğine göre, neden sadece sınırlar - gelen kimliği olarak kontrol edip, 0 <= id < packets.lengthemin ol packets[id]!=nullve sonra yapmıyorsun packets[id].execute(data)?
Lawrence Dol

evet, geç yanıt için özür dilerim, buna tekrar baktım .. ve ne cehenneme düşündüğüm hakkında hiçbir fikrim yok.
Jeremy Trifilo

0

Bayt kodu seviyesinde, konu değişkeni, Runtime tarafından yüklenen yapılandırılmış .class dosyasındaki bir bellek adresinden işlemci kaydına yalnızca bir kez yüklenir ve bu bir switch deyimindedir; Oysa bir if-ifadesinde, kod derleyen DE'niz tarafından farklı bir jvm talimatı üretilir ve bu, her bir değişkenin bir sonraki if-ifadesiyle aynı değişken kullanılmasına rağmen, kayıtlara yüklenmesini gerektirir. Assembly dilinde kodlamayı biliyorsanız, bu sıradan olacaktır; Java tarafından derlenen cox'lar bayt kodu veya doğrudan makine kodu olmamasına rağmen, buradaki koşullu kavram hala tutarlıdır. Açıklarken daha derin tekniklikten kaçınmaya çalıştım. Umarım konsepti netleştirmiş ve gizemi çözmüşümdür. Teşekkür ederim.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.