Perché cambiare è più veloce di se


116

Molti libri Java descrivono l' switchistruzione come più veloce if elsedell'istruzione. Ma non ho scoperto da nessuna parte perché il passaggio è più veloce di se .

Esempio

Ho una situazione in cui devo scegliere un elemento qualsiasi tra due. Posso usare entrambi gli usi

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

o

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

considerando l'articolo e il PANE è un valore int costante.

Nell'esempio sopra quale è più veloce in azione e perché?


Forse questa è una risposta anche per java: stackoverflow.com/questions/767821/…
Tobias

19
In generale, da Wikipedia : se l'intervallo di valori di input è identificabilmente 'piccolo' e presenta solo pochi spazi vuoti, alcuni compilatori che incorporano un ottimizzatore possono effettivamente implementare l'istruzione switch come una tabella di diramazione o un array di puntatori a funzione indicizzati invece di un lunga serie di istruzioni condizionali. Ciò consente all'istruzione switch di determinare istantaneamente quale ramo eseguire senza dover passare attraverso un elenco di confronti.
Felix Kling

La risposta principale a questa domanda lo spiega abbastanza bene. Anche questo articolo spiega tutto abbastanza bene.
bezmax

Spero che nella maggior parte dei casi un compilatore ottimizzato sia in grado di generare codice con caratteristiche di prestazioni simili. In ogni caso, dovresti chiamare molti milioni di volte per notare qualsiasi differenza.
Mitch Wheat

2
Dovresti diffidare dei libri che fanno affermazioni come questa senza spiegazioni / prove / ragionamenti.
matt b

Risposte:


110

Perché ci sono bytecode speciali che consentono una valutazione efficiente dell'istruzione switch in molti casi.

Se implementato con istruzioni IF avresti un controllo, un salto alla clausola successiva, un controllo, un salto alla clausola successiva e così via. Con lo switch la JVM carica il valore da confrontare e scorre la tabella dei valori per trovare una corrispondenza, che è più veloce nella maggior parte dei casi.


6
L'iterazione non si traduce in "controlla, salta"?
fivetwentysix

17
@fivetwentysix: No, fare riferimento a questo per informazioni: artima.com/underthehood/flowP.html . Citazione dall'articolo: Quando la JVM incontra un'istruzione tableswitch, può semplicemente controllare se la chiave è all'interno dell'intervallo definito da low e high. In caso contrario, prende l'offset del ramo predefinito. In tal caso, sottrae solo basso dalla chiave per ottenere un offset nell'elenco degli offset di diramazione. In questo modo, può determinare l'offset del ramo appropriato senza dover controllare il valore di ogni caso.
bezmax

1
(i) a switchnon può essere tradotto in tableswitchun'istruzione bytecode - può diventare lookupswitchun'istruzione che si comporta in modo simile a un if / else (ii) anche tableswitchun'istruzione bytecode può essere compilata in una serie di if / else dal JIT, a seconda di fattori come il numero di cases.
assylias


34

Una switchdichiarazione non è sempre più veloce di un ifcomunicato. Scala meglio di un lungo elenco di if-elseistruzioni in quanto switchpuò eseguire una ricerca basata su tutti i valori. Tuttavia, per una breve condizione non sarà più veloce e potrebbe essere più lento.


5
Si prega di limitare "lungo". Maggiore di 5? Maggiore di 10? o più come 20 - 30?
vanderwyst

11
Ho il sospetto che dipenda. Per me i suoi 3 o più suggerimenti switchsarebbero più chiari se non più veloci.
Peter Lawrey

In quali condizioni potrebbe essere più lento?
Eric

1
@Eric è più lento per un piccolo numero di valori esp String o int che sono sparsi.
Peter Lawrey

8

La JVM corrente ha due tipi di codici byte dello switch: LookupSwitch e TableSwitch.

Ogni caso in un'istruzione switch ha un offset intero, se questi offset sono contigui (o per lo più contigui senza grandi spazi) (caso 0: caso 1: caso 2, ecc.), Viene utilizzato TableSwitch.

Se gli offset sono distribuiti con ampi spazi (caso 0: caso 400: caso 93748 :, ecc.), Viene utilizzato LookupSwitch.

La differenza, in breve, è che TableSwitch viene eseguito in tempo costante perché a ciascun valore all'interno dell'intervallo di valori possibili viene assegnato uno specifico offset di byte-code. Quindi, quando dai all'istruzione un offset di 3, sa di saltare avanti di 3 per trovare il ramo corretto.

L'opzione di ricerca utilizza una ricerca binaria per trovare il ramo di codice corretto. Funziona in tempo O (log n), che è ancora buono, ma non il migliore.

Per ulteriori informazioni su questo, vedere qui: Differenza tra LookupSwitch di JVM e TableSwitch?

Quindi, per quanto riguarda quale è il più veloce, usa questo approccio: se hai 3 o più casi i cui valori sono consecutivi o quasi consecutivi, usa sempre un interruttore.

Se hai 2 casi, usa un'istruzione if.

Per qualsiasi altra situazione, il passaggio è molto probabile veloce, ma non è garantito, poiché la ricerca binaria in LookupSwitch potrebbe colpire uno scenario negativo.

Inoltre, tieni presente che la JVM eseguirà le ottimizzazioni JIT sulle istruzioni if ​​che cercheranno di posizionare il ramo più attivo per primo nel codice. Questa operazione è chiamata "Previsione del ramo". Per ulteriori informazioni su questo, vedere qui: https://dzone.com/articles/branch-prediction-in-java

Le tue esperienze possono variare. Non so che la JVM non esegua un'ottimizzazione simile su LookupSwitch, ma ho imparato a fidarmi delle ottimizzazioni JIT e non cercare di superare in astuzia il compilatore.


1
Da quando ho pubblicato questo articolo, ho notato che "espressioni di commutazione" e "corrispondenza di modelli" arriveranno in Java, forse non appena Java 12. openjdk.java.net/jeps/325 openjdk.java.net/jeps/305 Niente è ancora concreto, ma sembra che questi renderanno switchuna caratteristica del linguaggio ancora più potente. La corrispondenza dei modelli, ad esempio, consentirà instanceofricerche molto più fluide e performanti . Tuttavia, penso che sia lecito ritenere che per gli scenari switch / if di base, la regola che ho menzionato verrà comunque applicata.
HesNotTheStig

1

Quindi, se prevedi di avere un sacco di pacchetti di memoria non è davvero un grande costo in questi giorni e gli array sono piuttosto veloci. Inoltre, non puoi fare affidamento su un'istruzione switch per generare automaticamente una tabella di salto e in quanto tale è più facile generare lo scenario della tabella di salto da soli. Come puoi vedere nell'esempio seguente, assumiamo un massimo di 255 pacchetti.

Per ottenere il risultato di seguito è necessario astrazione .. Non ho intenzione di spiegare come funziona, quindi spero che tu ne abbia una comprensione.

L'ho aggiornato per impostare la dimensione del pacchetto su 255 se ne hai bisogno di più di quello che dovrai fare un controllo dei limiti per (id <0) || (id> lunghezza).

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

Modifica poiché uso molto una tabella di salto in C ++ ora mostrerò un esempio di una tabella di salto del puntatore a funzione. Questo è un esempio molto generico, ma l'ho eseguito e funziona correttamente. Tieni presente che devi impostare il puntatore su NULL, C ++ non lo farà automaticamente come in 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;
}

Un altro punto che vorrei sollevare è il famoso Divide and Conquer. Quindi la mia idea di array sopra 255 potrebbe essere ridotta a non più di 8 istruzioni if ​​come scenario peggiore.

Cioè, ma tieni presente che diventa disordinato e difficile da gestire velocemente e il mio altro approccio è generalmente migliore, ma questo è utilizzato nei casi in cui gli array non lo tagliano. Devi capire il tuo caso d'uso e quando ogni situazione funziona meglio. Proprio come non vorresti usare nessuno di questi approcci se avessi solo pochi controlli.

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
Perché la doppia indiretta? Dal momento che l'ID deve essere comunque vincolato, perché non limitarsi a controllare l'ID in entrata come 0 <= id < packets.lengthe assicurarsi packets[id]!=nulle poi fare packets[id].execute(data)?
Lawrence Dol

sì, scusa per la risposta in ritardo, ho guardato di nuovo questo .. e non ho idea di cosa diavolo stavo pensando Ho aggiornato il post lol e ho limitato i pacchetti alla dimensione di un byte senza segno, quindi non sono necessari controlli di lunghezza.
Jeremy Trifilo

0

A livello di bytecode, la variabile oggetto viene caricata solo una volta nel registro del processore da un indirizzo di memoria nel file .class strutturato caricato da Runtime, e questo è in un'istruzione switch; mentre in un'istruzione if, un'istruzione jvm diversa viene prodotta dal DE di compilazione del codice e ciò richiede che ogni variabile venga caricata nei registri sebbene venga utilizzata la stessa variabile come nella successiva istruzione if precedente. Se conosci la codifica in linguaggio assembly, questo sarebbe un luogo comune; sebbene i cox compilati in java non siano bytecode, o codice macchina diretto, il concetto condizionale di questo è ancora coerente. Ebbene, ho cercato di evitare tecnicismi più approfonditi durante la spiegazione. Spero di aver reso il concetto chiaro e demistificato. Grazie.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.