Eine HashMap hat im Wesentlichen eine Leistung von O(1), während ein Switch-Zustand entweder O(1) oder O(log(n)) haben kann, je nachdem, ob der Compiler einen Tableswitch oder einen Lookup-Switch verwendet.
Verständlicherweise, wenn eine switch-Anweisung als solche geschrieben wird,
switch (int) {
case 1:
case 2:
case 3:
case 4:
default:
}
dann würde es einen Tabellenschalter verwenden und einen eindeutigen Leistungsvorteil gegenüber einer Standard-HashMap haben. Aber was ist, wenn die switch-Anweisung spärlich ist? Dies wären zwei Beispiele, die ich vergleichen würde:
HashMap<Integer, String> example = new HashMap<Integer, String>() {{
put(1, "a");
put(10, "b");
put(100, "c");
put(1000, "d");
}};
.
switch (int) {
case 1:
return "a";
case 10:
return "b";
case 100:
return "c";
case 1000:
return "d";
default:
return null;
}
Was würde mehr Durchsatz liefern, ein Lookupswitch oder HashMap? Gibt der Overhead der HashMap dem Lookupswitch frühzeitig einen Vorteil, lässt aber mit zunehmender Anzahl von Fällen/Einträgen nach?
Bearbeiten: Ich habe einige Benchmarks mit JMH ausprobiert, hier sind meine Ergebnisse und der verwendete Code. https://gist.github.com/mooman219/bebbdc047889c7cfe612
Wie Sie bereits erwähnt haben, hat die Lookupswitch-Anweisung die HashTable übertroffen. Ich frage mich aber immer noch warum.
Wie bei so ziemlich jeder Leistungsfrage: Sie müssen sie messen. stackoverflow.com/q/504103/139010 Ich freue mich auf Ihre Ergebnisse :)
– Mattball
16. Januar 2015 um 22:32 Uhr
Eine gute Standardantwort ist, Ihnen zu sagen, dass Sie den Unterschied messen sollen … Ich würde erwarten, dass die switch-Anweisung schneller ist, da sie weniger Anweisungen enthalten sollte. Mit C und GCC wird ein Schalter je nach Kontext als if/elseif-Ketten, Sprungtabellen oder was nicht implementiert (z. B. wie viele Fälle im Schalter, Indizierung usw.).
– Morten Jensen
16. Januar 2015 um 22:32 Uhr
Zusätzlich zu der weitaus geringeren Komplexität hat der Lookupswitch-Mechanismus den wichtigen Vorteil der Referenzlokalität. Die Hashmap muss 1. Ihren Integer-Schlüssel dereferenzieren; 2. das Bucket-Array dereferenzieren; 3. dereferenzieren Sie die Node-Instanz im Array am Index, der von 1 abgeleitet ist; 4. den Integer-Schlüssel des Knotens dereferenzieren, um sicherzustellen, dass er gleich dem angeforderten Schlüssel ist; 5. Dereferenzieren Sie schließlich den Rückgabewert von der Node.
– Marko Topolnik
16. Januar 2015 um 22:44 Uhr
@MattBall Ich werde ein JMH starten und sehen, wie es für Schalter/Hashmaps unterschiedlicher Größe läuft.
– Jo C
16. Januar 2015 um 23:37 Uhr
Es könnte interessant sein, dasselbe mit einem String-Schlüssel anstelle von int auszuführen.
Switches sind immer so schnell, wenn nicht sogar schneller als Hash-Maps. Switch-Anweisungen werden in direkte Nachschlagetabellen umgewandelt. Bei Integer-Werten (ints, enums, shorts, longs) ist es ein direkter Lookup/JMP zur Anweisung. Es muss kein zusätzliches Hashing durchgeführt werden. Im Fall eines Strings wird der String-Hash für die Case-Anweisungen vorberechnet und der Hashcode des Eingabe-Strings verwendet, um zu bestimmen, wohin gesprungen werden soll. Im Falle einer Kollision führt es eine if/else-Kette aus. Jetzt denken Sie vielleicht: “Das ist dasselbe wie HashMap, oder?” Aber das stimmt nicht. Der Hash-Code für die Suche wird zur Kompilierzeit berechnet und nicht basierend auf der Anzahl der Elemente reduziert (geringere Kollisionswahrscheinlichkeit).
Switches haben O(1) Lookup, nicht O(n). (Ok, in Wahrheit werden Schalter für eine kleine Anzahl von Elementen in if/else-Anweisungen umgewandelt. Dies bietet eine bessere Codelokalität und vermeidet zusätzliche Speichersuchen. Bei vielen Elementen werden Schalter jedoch in die oben erwähnte Nachschlagetabelle geändert).
Sie können hier mehr darüber lesen. Wie funktioniert Javas Schalter unter der Haube?
Habe ich gesagt, welches schneller ist? Das habe ich nicht gesagt. Meine Antwort ist, welche in welchem Fall geeignet ist und warum wir uns keine Gedanken über die Leistung für den Fall machen müssen, da wir tonnenweise andere Anwendungsfälle haben, die wir zur Optimierung der Leistung benötigen, anstatt diesen einfachen Fall.
– LHA
10. September 2018 um 16:14 Uhr
Sie sagten, Switches hat O (1), es ist NICHT für alle Sprachen/Compiler korrekt. Es hängt vom Compiler ab und im schlimmsten Fall ist es O(n).
– LHA
10. September 2018 um 16:17 Uhr
LHA
Es hängt davon ab, ob:
Wenn es ein paar Artikel sind | feste Gegenstände. Verwenden Sie den Schalter, wenn Sie können (im schlimmsten Fall O (n))
Wenn es viele Elemente gibt ODER Sie zukünftige Elemente hinzufügen möchten, ohne viel Code zu ändern —> Hash-Map verwenden (Zugriffszeit wird als konstante Zeit betrachtet)
Sie sollten NICHT versuchen, die Leistung für den Fall zu verbessern, da der Unterschied in der Ausführungszeit Nanosekunden beträgt. Konzentrieren Sie sich einfach auf die Lesbarkeit/Wartbarkeit Ihres Codes. Lohnt es sich, einen einfachen Fall zu optimieren, um ein paar Nanosekunden zu verbessern?
Warum sind viele Elemente schlecht für die Switch-Anweisung? Ich habe gehört, dass der Compiler Optimierungen und Binärbäume in Switch-Anweisungen für uns durchführt.
– KRoy
30. August 2017 um 15:06 Uhr
Das ist falsch. Wenn Sie wechseln können, wechseln Sie. Es wird immer so schnell sein, wenn nicht sogar schneller als eine Hashmap. Für ganzzahlige Werte und Enum-Werte wird sie in eine Nachschlagetabelle im Arbeitsspeicher umgewandelt, die nicht die Kosten des Hashings verursacht. Für eine Zeichenfolge erstellt es die HashMap und verwendet diese zum Umschalten. Das einzige, was wechseln könnte, ist langsamer als ein if/else-Block, da die Codelokalität niedrig ist.
– Cogmann
6. September 2018 um 20:36 Uhr
Ich mag es wirklich nicht, wenn Leute Dinge sagen wie „Sie sollten sich keine Sorgen um die Leistung machen“. Ja, in diesem Fall mag es durchaus stimmen, aber im Großen und Ganzen produziert unsere Industrie TAUSENDE von Softwarepaketen im Moment mit schrecklicher, inakzeptabler Leistung (angesichts der Hardware, die wir derzeit verwenden). Das Problem ist wirklich nicht, dass die Leute denken zu viel Über die Leistung, ganz im Gegenteil. Eben wenn Einige Leute würden zu viel darüber nachdenken, nun, es ist definitiv ein viel geringeres Problem als der “YAGNI” -Ansatz, wenn er auf die Leistungsoptimierung angewendet wird. :/
– Per Lundberg
22. März 2019 um 14:13 Uhr
Kann dieses „Mach dir keine Sorgen um die Leistung“ nicht mehr hören. Das ist der Grund, warum Leute behaupten, Java sei langsam. Bei heutigen Anwendungen ist es nicht unrealistisch, Milliarden (und mehr) von Ereignissen pro Sekunde zu haben. Insbesondere für die Entwicklung von Bibliotheken ist es daher äußerst wichtig, sich um die Leistung zu kümmern. Also ja, auch Nanosekunden können wichtig sein. Die Frage will nach dem Codestil, also beantworte bitte einfach die Frage … wenn du kannst.
– PeMa
20. Juni 2020 um 9:58 Uhr
@PerLundberg: Hast du gesehen, dass ich “Für deinen Fall” gesagt habe? – In diesem Fall bedeutet dies, dass sich der Eigentümer der Frage keine Gedanken über die Leistung machen sollte. Ich habe NICHT in allen Fällen gesagt, dass Sie sich keine Sorgen um die Leistung machen sollten. Wir müssen uns immer um die Leistung kümmern. Wir müssen unsere API auch für die Leistung optimieren, ABER NICHT ALLE SINGLE-SOURCE-CODELINIENOPTIMIERUNGEN? Denn Lesbarkeit/Wartbarkeit ist auch wichtig. Manchmal müssen wir eine Art Kompromiss eingehen.
– LHA
21. Juni 2020 um 0:59 Uhr
TL/DR
Basieren Sie auf der Lesbarkeit und Wartbarkeit des Codes. Beide kosten O (1) und bieten fast keinen Unterschied (obwohl Switches im Allgemeinen etwas schneller sind).
In diesem speziellen Fall wäre eine Karte schneller, da ein Schalter eine Adresse zurückgibt und dann zu dieser Adresse gehen muss, um den Rückgabewert zu identifizieren. (Ein seltenes Fallbeispiel). Wenn Ihr Switch sowieso nur Funktionen aufruft, wäre eine Map auch schneller.
Um die Dinge schneller zu machen, würde ich sicherstellen, dass numerische Fälle verwendet werden, und die Verwendung von Zeichenfolgen über Konstanten oder Enumeratoren (Typoskript) vermeiden.
(bearbeitet) Ich habe erwartungsgemäß bestätigt: Wie funktioniert der Schalter von Java unter der Haube? mit Schaltern.
Ausführlichere Antwort
Im Unkraut:
Eine Switch-Anweisung ist normalerweise leistungsstärker. Es erstellt eine Nachschlagetabelle und eine Goto-Referenz und beginnt an diesem Punkt. Es gibt jedoch Ausnahmen.
Wenn Sie einen einfachen Schalter wie return map.get(x) vs. switch(1=>’a’, 2=>’b’, etc) verwenden. Das liegt daran, dass die Zuordnung den gewünschten Wert direkt zurückgeben kann, wo der Schalter die Zuordnung der Adressen beendet und fortfährt, bis die Unterbrechung oder das Ende erreicht ist.
In jedem Fall sollten sie hinsichtlich der Ausführungskosten äußerst ähnlich sein.
Denken Sie an die Wartbarkeit und Lesbarkeit des Codes
Die Verwendung einer Karte entkoppelt die Daten, was den Vorteil der dynamischen Erstellung von „Switch“-Fällen bringen kann. Näheres unten.
Wenn Sie mehrere komplexe Funktionen/Prozesse handhaben müssen, ist es möglicherweise einfacher zu lesen/schreiben, wenn Sie stattdessen eine Karte verwenden. Vor allem, wenn die Switch-Anweisung 20 oder 30 Optionen überschreitet.
Persönlich verwendetes Fallbeispiel für Karten:
Ich verwende seit einiger Zeit das folgende Muster für Flussmittel (Redux/useReducer) in React-Anwendungen.
Ich erstelle eine zentrale Karte, in der ich den Trigger als Schlüssel abbilde und der Wert eine funktionale Referenz ist. Ich kann dann Fälle laden, wo und wann es sinnvoll ist.
Ursprünglich habe ich dies verwendet, um die Anwendungsfälle aufzuschlüsseln, um die Dateigröße zu reduzieren und Fälle mit ähnlicher Funktion organisierter zusammenzufassen. Obwohl ich es später so weiterentwickelt habe, dass es in Domänen geladen werden kann, und die Ereignisse und Daten in einem Domänen-Hook konfiguriert habe, wie useUser, useFeedback, useProfile usw.
Dadurch konnte ich den Standardzustand, Initialisierungsfunktionen, Ereignisse usw. in einer logischen Dateistruktur erstellen und den Platzbedarf bis zur Verwendung gering halten.
Eine Anmerkung zu beachten
Die Verwendung einer Karte erlaubt kein Drop-Through, obwohl die meisten Leute diesen Code sowieso als Geruch betrachten. Gleichzeitig schützt es vor versehentlichem Durchfallen.
sqlsword
In Ihrem Fall, da Sie eine verwenden Integer Schlüssel für Ihre HashMap und ein einfaches ‘int‘ für dein switch -Anweisung ist die Implementierung mit der besten Leistung die switch -Anweisung, es sei denn, die Anzahl der Durchläufe durch diesen Codeabschnitt ist sehr hoch (Zehn- oder Hunderttausende).
John Stamm
Wenn ich ein solches Beispiel habe, verwende ich Guava ImmutableMaps (sicher, Sie können auch Java 9 Builder verwenden).
private static final Map<String, String> EXAMPLE = ImmutableMaps.<String, String>>builder()
.put("a", "100")
.put("b", "200")
.build();
Auf diese Weise sind sie unveränderlich und werden nur einmal initiiert.
Manchmal verwende ich Strategiemuster auf diese Weise:
private static final Map<String, Command> EXAMPLE = ImmutableMaps.<String, String>>builder()
.put("a", new SomethingCool())
.put("b", new BCool())
.build();
private static final Command DEFAULT= new DefCommand();
Wie bei so ziemlich jeder Leistungsfrage: Sie müssen sie messen. stackoverflow.com/q/504103/139010 Ich freue mich auf Ihre Ergebnisse
:)
– Mattball
16. Januar 2015 um 22:32 Uhr
Eine gute Standardantwort ist, Ihnen zu sagen, dass Sie den Unterschied messen sollen … Ich würde erwarten, dass die switch-Anweisung schneller ist, da sie weniger Anweisungen enthalten sollte. Mit C und GCC wird ein Schalter je nach Kontext als if/elseif-Ketten, Sprungtabellen oder was nicht implementiert (z. B. wie viele Fälle im Schalter, Indizierung usw.).
– Morten Jensen
16. Januar 2015 um 22:32 Uhr
Zusätzlich zu der weitaus geringeren Komplexität hat der Lookupswitch-Mechanismus den wichtigen Vorteil der Referenzlokalität. Die Hashmap muss 1. Ihren Integer-Schlüssel dereferenzieren; 2. das Bucket-Array dereferenzieren; 3. dereferenzieren Sie die Node-Instanz im Array am Index, der von 1 abgeleitet ist; 4. den Integer-Schlüssel des Knotens dereferenzieren, um sicherzustellen, dass er gleich dem angeforderten Schlüssel ist; 5. Dereferenzieren Sie schließlich den Rückgabewert von der Node.
– Marko Topolnik
16. Januar 2015 um 22:44 Uhr
@MattBall Ich werde ein JMH starten und sehen, wie es für Schalter/Hashmaps unterschiedlicher Größe läuft.
– Jo C
16. Januar 2015 um 23:37 Uhr
Es könnte interessant sein, dasselbe mit einem String-Schlüssel anstelle von int auszuführen.
– Monothreaded
20. März 2015 um 7:22 Uhr