Warum ist der Wechsel schneller als wenn

116

Viele Java-Bücher beschreiben die switchAussage als schneller als die if elseAussage. Aber ich habe nirgendwo herausgefunden, warum der Wechsel schneller ist als wenn .

Beispiel

Ich habe eine Situation, in der ich einen von zwei Artikeln auswählen muss. Ich kann beide verwenden

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

oder

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

unter Berücksichtigung von item und BREAD ist ein konstanter int-Wert.

Was ist im obigen Beispiel schneller in Aktion und warum?

Jacob van Lingen
quelle
Vielleicht ist dies eine Antwort auch für Java: stackoverflow.com/questions/767821/…
Tobias
19
Im Allgemeinen aus Wikipedia : Wenn der Bereich der Eingabewerte identifizierbar "klein" ist und nur wenige Lücken aufweist, implementieren einige Compiler, die einen Optimierer enthalten, die switch-Anweisung möglicherweise tatsächlich als Verzweigungstabelle oder als Array indizierter Funktionszeiger anstelle von a lange Reihe von bedingten Anweisungen. Auf diese Weise kann die switch-Anweisung sofort bestimmen, welcher Zweig ausgeführt werden soll, ohne eine Liste von Vergleichen durchlaufen zu müssen.
Felix Kling
Die beste Antwort auf diese Frage erklärt es ziemlich gut. Dieser Artikel erklärt auch alles ziemlich gut.
Bezmax
Ich würde hoffen, dass ein optimierender Compiler in den meisten Fällen Code generieren kann, der ähnliche Leistungsmerkmale aufweist. In jedem Fall müssten Sie viele Millionen Mal anrufen, um einen Unterschied zu bemerken.
Mitch Wheat
2
Sie sollten sich vor Büchern hüten, die solche Aussagen ohne Erklärung / Beweis / Begründung machen.
Matt B

Antworten:

110

Weil es spezielle Bytecodes gibt, die in vielen Fällen eine effiziente Auswertung von switch-Anweisungen ermöglichen.

Wenn mit IF-Anweisungen implementiert, hätten Sie eine Prüfung, einen Sprung zur nächsten Klausel, eine Prüfung, einen Sprung zur nächsten Klausel und so weiter. Mit switch lädt die JVM den zu vergleichenden Wert und durchläuft die Wertetabelle, um eine Übereinstimmung zu finden, die in den meisten Fällen schneller ist.

Daniel
quelle
6
Wird die Iteration nicht in "überprüfen, springen" übersetzt?
Fivetwentysix
17
@fivetwentysix: Nein, Informationen hierzu finden Sie unter artima.com/underthehood/flowP.html . Zitat aus dem Artikel: Wenn die JVM auf eine Tabellenschalteranweisung stößt, kann sie einfach überprüfen, ob der Schlüssel innerhalb des durch niedrig und hoch definierten Bereichs liegt. Wenn nicht, wird der Standardzweigversatz verwendet. In diesem Fall wird nur ein niedriger Wert vom Schlüssel abgezogen, um einen Versatz in die Liste der Verzweigungsversätze aufzunehmen. Auf diese Weise kann der geeignete Verzweigungsversatz ermittelt werden, ohne dass jeder Fallwert überprüft werden muss.
bezmax
1
(i) a switchdarf nicht in einen tableswitchBytecode-Befehl übersetzt werden - es kann ein lookupswitchBefehl werden, der ähnlich wie ein if / else funktioniert. (ii) Selbst ein tableswitchBytecode-Befehl kann abhängig von den Faktoren von der JIT zu einer Reihe von if / else kompiliert werden wie die Anzahl von cases.
Assylias
1
tableswitchvs loopuswitch: stackoverflow.com/questions/10287700/…
Ciro Santilli 法轮功 冠状 病 六四. 法轮功
34

Eine switchAnweisung ist nicht immer schneller als eine ifAnweisung. Es skaliert besser als eine lange Liste von if-elseAnweisungen, da switcheine Suche basierend auf allen Werten durchgeführt werden kann. Für einen kurzen Zustand wird es jedoch nicht schneller und könnte langsamer sein.

Peter Lawrey
quelle
5
Bitte beschränken Sie "lang". Größer als 5? Größer als 10? oder eher 20 - 30?
Vanderwyst
11
Ich vermute, es kommt darauf an. Für mich switchwären seine 3 oder mehr Vorschläge klarer, wenn nicht schneller.
Peter Lawrey
Unter welchen Bedingungen könnte es langsamer sein?
Eric
1
@Eric ist es langsamer für eine kleine Anzahl von Werten, insbesondere String oder int, die spärlich sind.
Peter Lawrey
8

Die aktuelle JVM verfügt über zwei Arten von Switch-Byte-Codes: LookupSwitch und TableSwitch.

Jeder Fall in einer switch-Anweisung hat einen ganzzahligen Offset. Wenn diese Offsets zusammenhängend sind (oder größtenteils zusammenhängend ohne große Lücken) (Fall 0: Fall 1: Fall 2 usw.), wird TableSwitch verwendet.

Wenn die Offsets mit großen Lücken verteilt sind (Fall 0: Fall 400: Fall 93748: usw.), wird LookupSwitch verwendet.

Kurz gesagt, der Unterschied besteht darin, dass TableSwitch in konstanter Zeit ausgeführt wird, da jedem Wert innerhalb des Bereichs möglicher Werte ein spezifischer Bytecode-Offset zugewiesen wird. Wenn Sie der Anweisung einen Versatz von 3 geben, muss sie 3 vorausspringen, um den richtigen Zweig zu finden.

Der Suchschalter verwendet eine binäre Suche, um den richtigen Codezweig zu finden. Dies läuft in O (log n) Zeit, was immer noch gut, aber nicht die beste ist.

Weitere Informationen hierzu finden Sie hier: Unterschied zwischen LookupSwitch und TableSwitch von JVM?

Verwenden Sie diesen Ansatz, um festzustellen, welcher am schnellsten ist: Wenn Sie drei oder mehr Fälle haben, deren Werte aufeinanderfolgend oder nahezu aufeinanderfolgend sind, verwenden Sie immer einen Schalter.

Wenn Sie 2 Fälle haben, verwenden Sie eine if-Anweisung.

In jeder anderen Situation ist der Wechsel höchstwahrscheinlich schneller, aber nicht garantiert, da die Binärsuche in LookupSwitch auf ein schlechtes Szenario stoßen könnte.

Beachten Sie außerdem, dass die JVM JIT-Optimierungen für if-Anweisungen ausführt, die versuchen, den heißesten Zweig zuerst im Code zu platzieren. Dies wird als "Verzweigungsvorhersage" bezeichnet. Weitere Informationen hierzu finden Sie hier: https://dzone.com/articles/branch-prediction-in-java

Ihre Erfahrungen können variieren. Ich weiß nicht, dass die JVM keine ähnliche Optimierung auf LookupSwitch ausführt, aber ich habe gelernt, den JIT-Optimierungen zu vertrauen und nicht zu versuchen, den Compiler zu überlisten.

HesNotTheStig
quelle
1
Seit ich dies gepostet habe, ist mir aufgefallen, dass "Switch-Ausdrücke" und "Pattern Matching" nach Java kommen, möglicherweise sobald Java 12. openjdk.java.net/jeps/325 openjdk.java.net/jeps/305 Noch ist nichts konkret, aber es scheint, dass diese switcheine noch leistungsfähigere Sprachfunktion darstellen werden. Der Musterabgleich ermöglicht beispielsweise eine viel flüssigere und leistungsfähigere instanceofSuche. Ich denke jedoch, dass man davon ausgehen kann, dass für grundlegende Switch / If-Szenarien die von mir erwähnte Regel weiterhin gilt.
HesNotTheStig
1

Wenn Sie also vorhaben, viel Paketspeicher zu haben, sind die Kosten heutzutage nicht wirklich hoch und die Arrays sind ziemlich schnell. Sie können sich auch nicht auf eine switch-Anweisung verlassen, um automatisch eine Sprungtabelle zu generieren. Daher ist es einfacher, das Sprungtabellenszenario selbst zu generieren. Wie Sie im folgenden Beispiel sehen können, gehen wir von maximal 255 Paketen aus.

Um das folgende Ergebnis zu erhalten, brauchen Sie eine Abstraktion. Ich werde nicht erklären, wie das funktioniert. Hoffentlich haben Sie ein Verständnis dafür.

Ich habe dies aktualisiert, um die Paketgröße auf 255 festzulegen, wenn Sie mehr benötigen, als Sie für (id <0) || prüfen müssen (id> Länge).

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

Bearbeiten, da ich in C ++ häufig eine Sprungtabelle verwende, zeige ich jetzt ein Beispiel für eine Sprungtabelle für Funktionszeiger. Dies ist ein sehr allgemeines Beispiel, aber ich habe es ausgeführt und es funktioniert korrekt. Beachten Sie, dass Sie den Zeiger auf NULL setzen müssen. C ++ führt dies nicht automatisch wie in Java aus.

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

Ein weiterer Punkt, den ich ansprechen möchte, ist das berühmte Teilen und Erobern. Meine über 255 Array-Idee könnte also auf nicht mehr als 8 if-Anweisungen als Worst-Case-Szenario reduziert werden.

Dh aber denken Sie daran, es wird chaotisch und schwer, schnell zu verwalten, und mein anderer Ansatz ist im Allgemeinen besser, aber dies wird in Fällen verwendet, in denen Arrays es einfach nicht schneiden. Sie müssen Ihren Anwendungsfall herausfinden und wann jede Situation am besten funktioniert. Genauso wie Sie keinen dieser Ansätze verwenden möchten, wenn Sie nur wenige Überprüfungen haben.

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
quelle
2
Warum die doppelte Indirektion? Da die ID ohnehin eingeschränkt werden muss, warum nicht einfach die eingehende ID als überprüfen 0 <= id < packets.lengthund sicherstellen packets[id]!=nullund dann tun packets[id].execute(data)?
Lawrence Dol
Ja, entschuldigen Sie die verspätete Antwort. Ich habe keine Ahnung, was zum Teufel ich dachte. Ich habe den Beitrag aktualisiert und die Pakete auf die Größe eines vorzeichenlosen Bytes begrenzt, sodass keine Längenprüfungen erforderlich sind.
Jeremy Trifilo
0

Auf Bytecode-Ebene wird die Subjektvariable nur einmal von einer Speicheradresse in der von Runtime geladenen strukturierten .class-Datei in das Prozessorregister geladen, und dies ist in einer switch-Anweisung enthalten. Während in einer if-Anweisung eine andere jvm-Anweisung von Ihrem Code kompilierenden DE erzeugt wird, muss jede Variable in Register geladen werden, obwohl dieselbe Variable wie in der nächsten vorangegangenen if-Anweisung verwendet wird. Wenn Sie sich mit Codierung in Assemblersprache auskennen, ist dies an der Tagesordnung. Obwohl Java-kompilierte Coxen kein Bytecode oder direkter Maschinencode sind, ist das bedingte Konzept hiervon immer noch konsistent. Nun, ich habe versucht, tiefere technische Aspekte beim Erklären zu vermeiden. Ich hoffe, ich hatte das Konzept klar und entmystifiziert. Danke dir.

Vers Villalon Gamboa
quelle