Ich habe mich neulich mit einem Freund über diese beiden Schnipsel gestritten. Welches ist schneller und warum?
value = 5;
if (condition) {
value = 6;
}
und:
if (condition) {
value = 6;
} else {
value = 5;
}
Was ist, wenn value
es sich um eine Matrix handelt?
Hinweis: Ich weiß, dass value = condition ? 6 : 5;
es das gibt und ich erwarte, dass es schneller ist, aber es war keine Option.
Bearbeiten (vom Personal angefordert, da die Frage gerade gehalten wird):
- Bitte antworten Sie, indem Sie entweder die von Mainstream-Compilern (z. B. g ++, clang ++, vc, mingw ) generierte x86-Assembly in optimierten und nicht optimierten Versionen oder die MIPS-Assembly berücksichtigen .
- Wenn sich die Assembly unterscheidet, erklären Sie, warum eine Version schneller ist und wann ( z. B. "besser, da keine Verzweigung und Verzweigung das folgende Problem hat" ).
c++
performance
c++11
assembly
microbenchmark
Julien__
quelle
quelle
value = condition ? 6 : 5;
anstelle vonif/else
führt höchstwahrscheinlich dazu, dass derselbe Code generiert wird. Sehen Sie sich die Assembly-Ausgabe an, wenn Sie mehr wissen möchten.Antworten:
TL; DR: In nicht optimiertem Code scheint der Code grundsätzlich neu geschrieben zu werden,
if
ohneelse
dass er irrelevant effizienter erscheint, aber selbst wenn die grundlegendste Optimierungsstufe aktiviert istvalue = condition + 5
.Ich habe es ausprobiert und die Assembly für den folgenden Code generiert:
In gcc 6.3 mit deaktivierten Optimierungen (
-O0
) ist der relevante Unterschied:für
ifonly
, währendifelse
hatLetzteres sieht etwas weniger effizient aus, da es einen zusätzlichen Sprung hat, aber beide mindestens zwei und höchstens drei Aufgaben haben, es sei denn, Sie müssen wirklich jeden letzten Leistungsabfall herausholen (Hinweis: Wenn Sie nicht an einem Space Shuttle arbeiten, tun Sie dies nicht und selbst dann wahrscheinlich nicht) wird der Unterschied nicht spürbar sein.
Selbst mit der niedrigsten Optimierungsstufe (
-O1
) reduzieren sich beide Funktionen auf dasselbe:Das ist im Grunde das Äquivalent von
Angenommen, es
condition
ist null oder eins. Höhere Optimierungsstufen ändern die Ausgabe nicht wirklich, es sei denn, sie vermeiden dies,movzx
indem sie dasEAX
Register zu Beginn effizient auf Null setzen .Haftungsausschluss: Sie sollten sich wahrscheinlich nicht
5 + condition
selbst schreiben (obwohl der Standard garantiert, dass die Konvertierungtrue
in einen ganzzahligen Typ möglich ist1
), da Ihre Absicht für Personen, die Ihren Code lesen (einschließlich Ihres zukünftigen Selbst), möglicherweise nicht sofort offensichtlich ist. Mit diesem Code soll gezeigt werden, dass das, was der Compiler in beiden Fällen erzeugt, (praktisch) identisch ist. Ciprian Tomoiaga sagt es ganz gut in den Kommentaren:quelle
true
konvertierte Wertint
ergibt immer 1 Punkt. Wenn Ihr Zustand nur "wahr" und nicht derbool
Wert isttrue
, dann ist das natürlich eine ganz andere Sache.Die Antwort von CompuChip zeigt, dass
int
beide für dieselbe Baugruppe optimiert sind, sodass es keine Rolle spielt.Ich werde dies allgemeiner interpretieren, dh was ist, wenn
value
es sich um einen Typ handelt, dessen Konstruktionen und Aufgaben teuer sind (und Umzüge billig sind).dann
ist nicht optimal, da Sie in diesem Fall
condition
die unnötige Initialisierung durchführeninit1
und dann die Kopierzuweisung vornehmen.Das ist besser. Aber immer noch nicht optimal, wenn die Standardkonstruktion teuer ist und wenn die Kopierkonstruktion teurer ist als die Initialisierung.
Sie haben die bedingte Operatorlösung, die gut ist:
Wenn Ihnen der bedingte Operator nicht gefällt, können Sie eine Hilfsfunktion wie folgt erstellen:
Je nachdem was
init1
und wasinit2
können Sie auch berücksichtigen:Aber ich muss noch einmal betonen, dass dies nur relevant ist, wenn Konstruktion und Aufgaben für den gegebenen Typ wirklich teuer sind. Und selbst dann wissen Sie es nur durch Profilerstellung .
quelle
?:
Operators der Einführung einer neuen Funktion vorziehen . Schließlich werden Sie wahrscheinlich nicht nur die Bedingung an die Funktion übergeben, sondern auch einige Konstruktorargumente. Einige davon könnencreate()
je nach Zustand nicht einmal verwendet werden.In der Pseudo-Assemblersprache
kann oder kann nicht schneller sein als
abhängig davon, wie hoch entwickelt die tatsächliche CPU ist. Vom einfachsten zum schicksten:
Bei jeder CPU, die nach ungefähr 1990 hergestellt wurde, hängt eine gute Leistung von der Code-Anpassung im Befehls-Cache ab . Minimieren Sie daher im Zweifelsfall die Codegröße. Dies spricht für das erste Beispiel.
Bei einer einfachen " in der Reihenfolge, fünfstufigen Pipeline " -CPU, die immer noch ungefähr das ist, was Sie in vielen Mikrocontrollern erhalten, gibt es jedes Mal eine Pipelineblase, wenn ein Zweig - bedingt oder bedingungslos - genommen wird. Daher ist es auch wichtig, ihn zu minimieren die Anzahl der Verzweigungsanweisungen. Dies spricht auch für das erste Beispiel.
Etwas anspruchsvollere CPUs - ausgefallen genug, um " Out-of-Order-Ausführung " durchzuführen , aber nicht ausgefallen genug, um die bekanntesten Implementierungen dieses Konzepts zu verwenden - können Pipeline-Blasen verursachen, wenn sie auf Schreib-nach-Schreib-Gefahren stoßen . Dies spricht für das zweite Beispiel, in
r0
dem nur einmal geschrieben wird, egal was passiert. Diese CPUs sind normalerweise ausgefallen genug, um bedingungslose Verzweigungen im Anweisungsabruf zu verarbeiten. Sie tauschen also nicht nur die Schreib-nach-Schreib-Strafe gegen eine Verzweigungsstrafe.Ich weiß nicht, ob noch jemand diese Art von CPU herstellt. Die CPUs, die die "bekanntesten Implementierungen" der Ausführung außerhalb der Reihenfolge verwenden, können jedoch die weniger häufig verwendeten Anweisungen einschränken. Sie müssen sich daher bewusst sein, dass so etwas passieren kann. Ein echtes Beispiel sind falsche Datenabhängigkeiten von den Zielregistern in
popcnt
undlzcnt
auf Sandy Bridge-CPUs .Am höchsten Ende gibt die OOO-Engine für beide Codefragmente genau die gleiche Abfolge interner Operationen aus. Dies ist die Hardwareversion von "Keine Sorge, der Compiler generiert in beiden Fällen denselben Maschinencode." Die Codegröße spielt jedoch immer noch eine Rolle, und jetzt sollten Sie sich auch Gedanken über die Vorhersagbarkeit des bedingten Zweigs machen. Fehler bei der Verzweigungsvorhersage führen möglicherweise zu einer vollständigen Pipeline- Leerung , was sich nachteilig auf die Leistung auswirkt. Siehe Warum ist es schneller, ein sortiertes Array zu verarbeiten als ein unsortiertes Array? zu verstehen, wie viel Unterschied dies machen kann.
Wenn die Verzweigung ist sehr unberechenbar, und Ihre CPU verfügt über ein bedingtes-Set oder bedingte Bewegungsbefehle, ist dies die Zeit , sie zu benutzen:
oder
Die Conditional-Set-Version ist außerdem kompakter als jede andere Alternative. Wenn diese Anweisung verfügbar ist, ist sie praktisch garantiert die richtige Sache für dieses Szenario, selbst wenn die Verzweigung vorhersehbar war. Die Conditional-Move-Version erfordert ein zusätzliches Scratch-Register und verschwendet immer
li
die Versand- und Ausführungsressourcen eines Befehls. Wenn der Zweig tatsächlich vorhersehbar war, ist die Zweigversion möglicherweise schneller.quelle
In nicht optimiertem Code weist das erste Beispiel eine Variable immer einmal und manchmal zweimal zu. Das zweite Beispiel weist eine Variable nur einmal zu. Die Bedingung ist für beide Codepfade gleich, daher sollte dies keine Rolle spielen. Im optimierten Code hängt dies vom Compiler ab.
Wenn Sie diesbezüglich betroffen sind, generieren Sie wie immer die Assembly und sehen Sie, was der Compiler tatsächlich tut.
quelle
Was würde Sie denken lassen, dass einer von ihnen selbst der eine Liner schneller oder langsamer ist?
Mehr Codezeilen in einer höheren Sprache bieten dem Compiler mehr Möglichkeiten zum Arbeiten. Wenn Sie also eine allgemeine Regel dazu erstellen möchten, geben Sie dem Compiler mehr Code zum Arbeiten. Wenn der Algorithmus der gleiche ist wie in den obigen Fällen, würde man erwarten, dass der Compiler mit minimaler Optimierung dies herausfindet.
Keine große Überraschung, dass die erste Funktion in einer anderen Reihenfolge ausgeführt wurde, jedoch zur gleichen Ausführungszeit.
Hoffentlich haben Sie die Idee, dass Sie dies einfach hätten versuchen können, wenn nicht offensichtlich gewesen wäre, dass die verschiedenen Implementierungen nicht wirklich unterschiedlich waren.
Was eine Matrix angeht, bin ich mir nicht sicher, wie das wichtig ist.
Ich werde einfach den gleichen Wenn-Dann-Sonst-Wrapper um die großen Code-Blobs legen, sei es Wert = 5 oder etwas Komplizierteres. Ebenso wird der Vergleich, selbst wenn es sich um einen großen Code-Blob handelt, noch berechnet werden muss, und gleich oder ungleich etwas wird oft mit dem Negativ kompiliert, wenn (Bedingung) etwas getan wird, wird oft so kompiliert, als ob Bedingung nicht gehe.
Wir haben diese Übung gerade mit jemand anderem über Stackoverflow durchgeführt. Interessanterweise stellte dieser Mips-Compiler in diesem Fall nicht nur fest, dass die Funktionen gleich waren, sondern ließ eine Funktion einfach zur anderen springen, um Code-Platz zu sparen. Hab das hier aber nicht gemacht
einige weitere Ziele.
und Compiler
Mit diesem i-Code würde man erwarten, dass auch die verschiedenen Ziele übereinstimmen
Technisch gesehen gibt es bei einigen dieser Lösungen einen Leistungsunterschied. Manchmal ist das Ergebnis 5, wenn ein Sprung über das Ergebnis 6 Code ist, und umgekehrt, ist ein Zweig schneller als die Ausführung? man könnte argumentieren, aber die Ausführung sollte variieren. Dies ist jedoch eher eine if-Bedingung als eine if not-Bedingung im Code, die dazu führt, dass der Compiler die if-Funktion ausführt, wenn dieser Sprung über else ausgeführt wird. Dies ist jedoch nicht unbedingt auf den Codierungsstil zurückzuführen, sondern auf den Vergleich und die Fälle if und else in welcher Syntax auch immer.
quelle
Ok, da Assembly eines der Tags ist, gehe ich einfach davon aus, dass Ihr Code Pseudocode ist (und nicht unbedingt c), und übersetze ihn vom Menschen in 6502-Assembly.
1. Option (ohne sonst)
2. Option (mit sonst)
Annahmen: Bedingung ist im Y-Register. Setzen Sie dies in der ersten Zeile einer der beiden Optionen auf 0 oder 1, das Ergebnis wird im Akkumulator angezeigt.
Nachdem wir die Zyklen für beide Möglichkeiten eines jeden Falles gezählt haben, sehen wir, dass das 1. Konstrukt im Allgemeinen schneller ist; 9 Zyklen, wenn die Bedingung 0 ist, und 10 Zyklen, wenn die Bedingung 1 ist, während Option zwei ebenfalls 9 Zyklen ist, wenn die Bedingung 0 ist, aber 13 Zyklen, wenn die Bedingung 1 ist ( Zykluszählungen enthalten nicht die
BRK
am Ende ).Fazit:
If only
ist schneller alsIf-Else
konstruieren.Der Vollständigkeit halber hier eine optimierte
value = condition + 5
Lösung:Dies verkürzt unsere Zeit auf 8 Zyklen ( wieder ohne die
BRK
am Ende ).quelle