Der ternäre Operator ist doppelt so langsam wie ein if-else-Block?

246

Ich lese überall , dass ternäre Operator soll schneller sein als oder zumindest gleich wie, sein Äquivalent if- elseBlock.

Ich habe jedoch den folgenden Test durchgeführt und festgestellt, dass dies nicht der Fall ist:

Random r = new Random();
int[] array = new int[20000000];
for(int i = 0; i < array.Length; i++)
{
    array[i] = r.Next(int.MinValue, int.MaxValue);
}
Array.Sort(array);

long value = 0;
DateTime begin = DateTime.UtcNow;

foreach (int i in array)
{
    if (i > 0)
    {
        value += 2;
    }
    else
    {
        value += 3;
    }
    // if-else block above takes on average 85 ms

    // OR I can use a ternary operator:
    // value += i > 0 ? 2 : 3; // takes 157 ms
}
DateTime end = DateTime.UtcNow;
MessageBox.Show("Measured time: " + (end-begin).TotalMilliseconds + " ms.\r\nResult = " + value.ToString());

Mein Computer brauchte 85 ms, um den obigen Code auszuführen. Aber wenn ich kommentieren Sie die if- elseBrocken, und Kommentar- der ternäre Operator Linie, wird es etwa 157 ms dauern.

Warum passiert dies?

user1032613
quelle
96
Das erste, was behoben werden muss: Nicht DateTimezur Messung der Leistung verwenden. Verwenden Sie Stopwatch. Als nächstes etwas länger - das ist eine sehr kurze Zeit zum Messen.
Jon Skeet
49
Verwenden Sie beim Erstellen des RandomObjekts einen Startwert, damit er immer dieselbe Reihenfolge aufweist. Wenn Sie unterschiedlichen Code mit unterschiedlichen Daten testen, können Sie sehr gut Leistungsunterschiede feststellen.
Guffa
12
Haben Sie auch versucht, es im Release-Modus mit aktivierten Compiler-Optimierungen und ohne angeschlossenen Debugger zu kompilieren / auszuführen?
Chris Sinclair
7
@ LarryOBrien: Interessante Aufnahme. Ich habe gerade einen kurzen LINQPad-Test durchgeführt und mit dem sortierten Array nicht sortierte Ergebnisse erzielt oder nicht. Tatsächlich reproduziere ich damit sortiert den gleichen gemeldeten Geschwindigkeitsunterschied. Durch Entfernen der Sortierung wird auch der Zeitunterschied entfernt.
Chris Sinclair
39
Der Punkt hier ist, dass Leistungstest-Mikrooptimierungen schwierig sind . Praktisch alle Dinge, die Sie in Ihrem Ergebnis beobachten, hängen mit Fehlern in Ihrem Testcode zusammen, nicht mit Unterschieden im aussagekräftigen Code. Wenn Sie die hier aufgeführten Probleme beheben, wird es weitere geben, das kann ich Ihnen versichern. Die Moral der Geschichte, beschäftigen Sie sich nicht mit Mikrooptimierungen oder versuchen Sie sie überhaupt zu testen. Wenn der Code tatsächlich schwer zu messen ist, bedeutet dies, dass er nicht langsam genug ist, um einen Engpass zu verursachen. ignoriere es.
Servy

Antworten:

376

Um diese Frage zu beantworten, untersuchen wir den Assembler-Code, der von den JITs X86 und X64 für jeden dieser Fälle erstellt wurde.

X86, wenn / dann

    32:                 foreach (int i in array)
0000007c 33 D2                xor         edx,edx 
0000007e 83 7E 04 00          cmp         dword ptr [esi+4],0 
00000082 7E 1C                jle         000000A0 
00000084 8B 44 96 08          mov         eax,dword ptr [esi+edx*4+8] 
    33:                 {
    34:                     if (i > 0)
00000088 85 C0                test        eax,eax 
0000008a 7E 08                jle         00000094 
    35:                     {
    36:                         value += 2;
0000008c 83 C3 02             add         ebx,2 
0000008f 83 D7 00             adc         edi,0 
00000092 EB 06                jmp         0000009A 
    37:                     }
    38:                     else
    39:                     {
    40:                         value += 3;
00000094 83 C3 03             add         ebx,3 
00000097 83 D7 00             adc         edi,0 
0000009a 42                   inc         edx 
    32:                 foreach (int i in array)
0000009b 39 56 04             cmp         dword ptr [esi+4],edx 
0000009e 7F E4                jg          00000084 
    30:             for (int x = 0; x < iterations; x++)
000000a0 41                   inc         ecx 
000000a1 3B 4D F0             cmp         ecx,dword ptr [ebp-10h] 
000000a4 7C D6                jl          0000007C 

X86, ternär

    59:                 foreach (int i in array)
00000075 33 F6                xor         esi,esi 
00000077 83 7F 04 00          cmp         dword ptr [edi+4],0 
0000007b 7E 2D                jle         000000AA 
0000007d 8B 44 B7 08          mov         eax,dword ptr [edi+esi*4+8] 
    60:                 {
    61:                     value += i > 0 ? 2 : 3;
00000081 85 C0                test        eax,eax 
00000083 7F 07                jg          0000008C 
00000085 BA 03 00 00 00       mov         edx,3 
0000008a EB 05                jmp         00000091 
0000008c BA 02 00 00 00       mov         edx,2 
00000091 8B C3                mov         eax,ebx 
00000093 8B 4D EC             mov         ecx,dword ptr [ebp-14h] 
00000096 8B DA                mov         ebx,edx 
00000098 C1 FB 1F             sar         ebx,1Fh 
0000009b 03 C2                add         eax,edx 
0000009d 13 CB                adc         ecx,ebx 
0000009f 89 4D EC             mov         dword ptr [ebp-14h],ecx 
000000a2 8B D8                mov         ebx,eax 
000000a4 46                   inc         esi 
    59:                 foreach (int i in array)
000000a5 39 77 04             cmp         dword ptr [edi+4],esi 
000000a8 7F D3                jg          0000007D 
    57:             for (int x = 0; x < iterations; x++)
000000aa FF 45 E4             inc         dword ptr [ebp-1Ch] 
000000ad 8B 45 E4             mov         eax,dword ptr [ebp-1Ch] 
000000b0 3B 45 F0             cmp         eax,dword ptr [ebp-10h] 
000000b3 7C C0                jl          00000075 

X64, wenn / dann

    32:                 foreach (int i in array)
00000059 4C 8B 4F 08          mov         r9,qword ptr [rdi+8] 
0000005d 0F 1F 00             nop         dword ptr [rax] 
00000060 45 85 C9             test        r9d,r9d 
00000063 7E 2B                jle         0000000000000090 
00000065 33 D2                xor         edx,edx 
00000067 45 33 C0             xor         r8d,r8d 
0000006a 4C 8B 57 08          mov         r10,qword ptr [rdi+8] 
0000006e 66 90                xchg        ax,ax 
00000070 42 8B 44 07 10       mov         eax,dword ptr [rdi+r8+10h] 
    33:                 {
    34:                     if (i > 0)
00000075 85 C0                test        eax,eax 
00000077 7E 07                jle         0000000000000080 
    35:                     {
    36:                         value += 2;
00000079 48 83 C5 02          add         rbp,2 
0000007d EB 05                jmp         0000000000000084 
0000007f 90                   nop 
    37:                     }
    38:                     else
    39:                     {
    40:                         value += 3;
00000080 48 83 C5 03          add         rbp,3 
00000084 FF C2                inc         edx 
00000086 49 83 C0 04          add         r8,4 
    32:                 foreach (int i in array)
0000008a 41 3B D2             cmp         edx,r10d 
0000008d 7C E1                jl          0000000000000070 
0000008f 90                   nop 
    30:             for (int x = 0; x < iterations; x++)
00000090 FF C1                inc         ecx 
00000092 41 3B CC             cmp         ecx,r12d 
00000095 7C C9                jl          0000000000000060 

X64, ternär

    59:                 foreach (int i in array)
00000044 4C 8B 4F 08          mov         r9,qword ptr [rdi+8] 
00000048 45 85 C9             test        r9d,r9d 
0000004b 7E 2F                jle         000000000000007C 
0000004d 45 33 C0             xor         r8d,r8d 
00000050 33 D2                xor         edx,edx 
00000052 4C 8B 57 08          mov         r10,qword ptr [rdi+8] 
00000056 8B 44 17 10          mov         eax,dword ptr [rdi+rdx+10h] 
    60:                 {
    61:                     value += i > 0 ? 2 : 3;
0000005a 85 C0                test        eax,eax 
0000005c 7F 07                jg          0000000000000065 
0000005e B8 03 00 00 00       mov         eax,3 
00000063 EB 05                jmp         000000000000006A 
00000065 B8 02 00 00 00       mov         eax,2 
0000006a 48 63 C0             movsxd      rax,eax 
0000006d 4C 03 E0             add         r12,rax 
00000070 41 FF C0             inc         r8d 
00000073 48 83 C2 04          add         rdx,4 
    59:                 foreach (int i in array)
00000077 45 3B C2             cmp         r8d,r10d 
0000007a 7C DA                jl          0000000000000056 
    57:             for (int x = 0; x < iterations; x++)
0000007c FF C1                inc         ecx 
0000007e 3B CD                cmp         ecx,ebp 
00000080 7C C6                jl          0000000000000048 

Erstens: Warum ist der X86-Code so viel langsamer als X64?

Dies ist auf die folgenden Merkmale des Codes zurückzuführen:

  1. X64 verfügt über mehrere zusätzliche Register, und jedes Register ist 64-Bit. Auf diese Weise kann die X64-JIT die innere Schleife vollständig unter Verwendung von Registern ausführen, abgesehen vom Laden iaus dem Array, während die X86-JIT mehrere Stapeloperationen (Speicherzugriff) in die Schleife einfügt.
  2. valueist eine 64-Bit-Ganzzahl, für die auf X86 ( addgefolgt von adc) 2 Maschinenbefehle erforderlich sind, auf X64 ( add) jedoch nur 1 .

Zweitens: Warum ist der ternäre Operator sowohl auf X86 als auch auf X64 langsamer?

Dies ist auf einen geringfügigen Unterschied in der Reihenfolge der Operationen zurückzuführen, der sich auf den Optimierer der JIT auswirkt. JIT des ternären Operators, anstatt direkt zu codieren2 und 3in den addMaschinenanweisungen selbst zu , erstellt die JIT eine Zwischenvariable (in einem Register), um das Ergebnis zu speichern. Dieses Register wird dann vor dem Hinzufügen von 32 Bit auf 64 Bit vorzeichenerweitert value. Da all dies in Registern für X64 ausgeführt wird, wird die Nettoauswirkung trotz der signifikanten Zunahme der Komplexität für den ternären Operator etwas minimiert.

Die X86-JIT ist dagegen stärker betroffen, da durch Hinzufügen eines neuen Zwischenwerts in der inneren Schleife ein weiterer Wert "verschüttet" wird, was zu mindestens 2 zusätzlichen Speicherzugriffen in der inneren Schleife führt (siehe die Zugriffe) bis [ebp-14h]im ternären X86-Code).

Sam Harwell
quelle
18
Der Compiler kann das Ternär genauso gut zu einem if-else erweitern.
Dezfowler
13
Beachten Sie, dass x86 nur langsamer ist, wenn Sie ternär verwenden - es ist genauso schnell wie x64, wenn Sie if / else verwenden . Die zu beantwortende Frage lautet also: "Warum ist der X86-Code bei Verwendung des ternären Operators so viel langsamer als X64?"
Eren Ersönmez
18
Sicher gibt es keinen guten Grund dafür und MS sollte es "reparieren" - da Ternary effektiv nur eine kürzere Syntax für if / else ist?! Sie würden sicherlich sowieso nicht erwarten, eine Leistungsstrafe zu zahlen.
Niico
6
@niico Es gibt nichts zu "reparieren" über den ternären Operator. Die Verwendung in diesem Fall führt zufällig zu einer anderen Registerzuordnung. In einem anderen Fall könnte es schneller sein als wenn / sonst, wie ich in meiner Antwort zu erklären versuchte.
Eren Ersönmez
6
@ ErenErsönmez: Sicher gibt es etwas zu reparieren. Das Optimierungs-Team kann die beiden Fälle sorgfältig analysieren und einen Weg finden, um den ternären Operator in diesem Fall genauso schnell wie sonst zu machen. Natürlich kann eine solche Lösung nicht durchführbar oder zu teuer sein.
Brian
63

EDIT: Alle Änderungen ... siehe unten.

Ich kann Ihre Ergebnisse auf der x64 CLR nicht reproduzieren, aber ich kann auf x86. Auf x64 sehe ich einen kleinen Unterschied (weniger als 10%) zwischen dem bedingten Operator und dem if / else, aber er ist viel kleiner als Sie sehen.

Ich habe die folgenden möglichen Änderungen vorgenommen:

  • In einer Konsolen-App ausführen
  • Erstellen Sie mit /o+ /debug-und führen Sie es außerhalb des Debuggers aus
  • Führen Sie beide Codeteile einmal aus, um sie zu JITEN, und dann viele Male, um die Genauigkeit zu erhöhen
  • Verwenden Stopwatch

Ergebnisse mit /platform:x64(ohne die "Ignorieren" -Zeilen):

if/else with 1 iterations: 17ms
conditional with 1 iterations: 19ms
if/else with 1000 iterations: 17875ms
conditional with 1000 iterations: 19089ms

Ergebnisse mit /platform:x86(ohne die "Ignorieren" -Zeilen):

if/else with 1 iterations: 18ms
conditional with 1 iterations: 49ms
if/else with 1000 iterations: 17901ms
conditional with 1000 iterations: 47710ms

Meine Systemdetails:

  • x64 i7-2720QM CPU bei 2,20 GHz
  • 64-Bit-Windows 8
  • .NET 4.5

Also im Gegensatz zu vorher, ich glaube , Sie werden einen echten Unterschied zu sehen - und es ist alles mit dem x86 - JIT zu tun. Ich möchte nicht genau was sagen den Unterschied verursacht - ich kann den Beitrag später mit weiteren Details aktualisieren, wenn ich mich die Mühe machen kann, auf cordbg zu gehen :)

Interessanterweise habe ich, ohne das Array zuerst zu sortieren, Tests, die mindestens 4,5x so lange dauern, zumindest auf x64. Ich vermute, dass dies mit der Verzweigungsvorhersage zu tun hat.

Code:

using System;
using System.Diagnostics;

class Test
{
    static void Main()
    {
        Random r = new Random(0);
        int[] array = new int[20000000];
        for(int i = 0; i < array.Length; i++)
        {
            array[i] = r.Next(int.MinValue, int.MaxValue);
        }
        Array.Sort(array);
        // JIT everything...
        RunIfElse(array, 1);
        RunConditional(array, 1);
        // Now really time it
        RunIfElse(array, 1000);
        RunConditional(array, 1000);
    }

    static void RunIfElse(int[] array, int iterations)
    {        
        long value = 0;
        Stopwatch sw = Stopwatch.StartNew();

        for (int x = 0; x < iterations; x++)
        {
            foreach (int i in array)
            {
                if (i > 0)
                {
                    value += 2;
                }
                else
                {
                    value += 3;
                }
            }
        }
        sw.Stop();
        Console.WriteLine("if/else with {0} iterations: {1}ms",
                          iterations,
                          sw.ElapsedMilliseconds);
        // Just to avoid optimizing everything away
        Console.WriteLine("Value (ignore): {0}", value);
    }

    static void RunConditional(int[] array, int iterations)
    {        
        long value = 0;
        Stopwatch sw = Stopwatch.StartNew();

        for (int x = 0; x < iterations; x++)
        {
            foreach (int i in array)
            {
                value += i > 0 ? 2 : 3;
            }
        }
        sw.Stop();
        Console.WriteLine("conditional with {0} iterations: {1}ms",
                          iterations,
                          sw.ElapsedMilliseconds);
        // Just to avoid optimizing everything away
        Console.WriteLine("Value (ignore): {0}", value);
    }
}
Jon Skeet
quelle
31
Die Frage, die immer noch jeder wissen möchte, ist, warum es überhaupt einen winzigen Unterschied gibt.
Brad M
1
@BradM: Nun, die IL wird anders sein, und jeder Unterschied könnte alle möglichen Dinge tun, wenn er JIT-kompiliert ist, und dann hat die CPU selbst böse Dinge getan.
Jon Skeet
4
@ JonSkeet FYI. hat Ihren Code genau so ausgeführt, wie Sie es erklärt haben. 19s vs 52s in x86 und 19s vs 21s in x64.
Eren Ersönmez
5
@ user1032613: Ich kann jetzt Ihre Ergebnisse reproduzieren. Siehe meine Bearbeitung. Entschuldigung, dass Sie schon einmal daran gezweifelt haben - es ist erstaunlich, welchen Unterschied eine Änderung der Architektur bewirken kann ...
Jon Skeet
3
@ BЈовић: In der Tat. Es begann damit, dass es überhaupt nicht reproduziert werden konnte, entwickelte sich aber im Laufe der Zeit. Es gibt keinen Grund an, aber ich dachte, es wären immer noch nützliche Informationen (z. B. der Unterschied zwischen x64 und x86), weshalb ich sie weggelassen habe.
Jon Skeet
43

Der Unterschied hat wirklich nicht viel damit zu tun, ob / sonst gegen ternär.

Wenn man sich die zerlegten Zerlegungen ansieht (ich werde sie hier nicht wiederholen, siehe Antwort von @ 280Z28), stellt sich heraus, dass man Äpfel und Orangen vergleicht . In einem Fall erstellen Sie zwei verschiedene +=Operationen mit konstanten Werten. Welche Sie auswählen, hängt von einer Bedingung ab. In dem anderen Fall erstellen Sie eine Stelle, +=an der der Wert hinzugefügt werden soll von einer Bedingung abhängt.

Wenn Sie wirklich vergleichen möchten, ob / else mit ternär ist, wäre dies ein fairer Vergleich (jetzt sind beide gleich "langsam", oder wir könnten sogar sagen, dass ternär etwas schneller ist):

int diff;
if (i > 0) 
    diff = 2;
else 
    diff = 3;
value += diff;

vs.

value += i > 0 ? 2 : 3;

Nun wird die Demontage für das if/elsewie unten gezeigt. Beachten Sie, dass dies etwas schlimmer ist als der ternäre Fall, da die Verwendung der Register für die Schleifenvariable ( i) ebenfalls beendet wird.

                if (i > 0)
0000009d  cmp         dword ptr [ebp-20h],0 
000000a1  jle         000000AD 
                {
                    diff = 2;
000000a3  mov         dword ptr [ebp-24h],2 
000000aa  nop 
000000ab  jmp         000000B4 
                }
                else
                {
                    diff = 3;
000000ad  mov         dword ptr [ebp-24h],3 
                }
                value += diff;
000000b4  mov         eax,dword ptr [ebp-18h] 
000000b7  mov         edx,dword ptr [ebp-14h] 
000000ba  mov         ecx,dword ptr [ebp-24h] 
000000bd  mov         ebx,ecx 
000000bf  sar         ebx,1Fh 
000000c2  add         eax,ecx 
000000c4  adc         edx,ebx 
000000c6  mov         dword ptr [ebp-18h],eax 
000000c9  mov         dword ptr [ebp-14h],edx 
000000cc  inc         dword ptr [ebp-28h] 
Eren Ersönmez
quelle
5
Wie wäre es mit dem Vergleich von Äpfeln und Orangen ?
Ken Kin
6
Nun, ich würde eigentlich nicht sagen, dass es Äpfel und Orangen vergleicht. Die beiden Varianten haben die gleiche Semantik , so der Optimierer konnte beiden Optimierungsvarianten versuchen , und wählen Sie je nachdem , was in effizienter ist jeder Fall.
Vlad
Ich habe den Test wie von Ihnen vorgeschlagen durchgeführt: Eine weitere Variable diffwurde eingeführt , aber ternär ist immer noch viel langsamer - überhaupt nicht das, was Sie gesagt haben. Haben Sie das Experiment durchgeführt, bevor Sie diese "Antwort" veröffentlicht haben?
user1032613
9

Bearbeiten:

Es wurde ein Beispiel hinzugefügt, das mit der if-else-Anweisung, jedoch nicht mit dem bedingten Operator ausgeführt werden kann.


Schauen Sie sich vor der Antwort bitte um [ Was ist schneller? ] auf Mr. Lipperts Blog. Und ich denke, die Antwort von Herrn Ersönmez ist hier die richtigste.

Ich versuche etwas zu erwähnen, das wir bei einer Programmiersprache auf hohem Niveau berücksichtigen sollten.

Zunächst einmal habe ich noch nie gehört, dass der bedingte Operator schneller sein soll oder die gleiche Leistung wie die if-else-Anweisung in C♯ .

Der Grund ist einfach: Was ist, wenn mit der if-else-Anweisung keine Operation ausgeführt wird:

if (i > 0)
{
    value += 2;
}
else
{
}

Die Anforderung des bedingten Operators besteht darin, dass auf beiden Seiten ein Wert vorhanden sein muss , und in C♯ muss auch beide Seiten :den gleichen Typ haben. Dies unterscheidet es nur von der if-else-Anweisung. So wird Ihre Frage zu einer Frage, wie die Anweisung des Maschinencodes erzeugt wird, so dass sich die Leistung unterscheidet.

Mit dem bedingten Operator ist es semantisch:

Unabhängig davon, welcher Ausdruck ausgewertet wird, gibt es einen Wert.

Aber mit if-else-Anweisung:

Wenn der Ausdruck als wahr ausgewertet wird, tun Sie etwas; Wenn nicht, machen Sie etwas anderes.

Ein Wert ist nicht unbedingt mit der if-else-Anweisung verbunden. Ihre Annahme ist nur mit Optimierung möglich.

Ein weiteres Beispiel, um den Unterschied zwischen ihnen zu demonstrieren, wäre wie folgt:

var array1=new[] { 1, 2, 3 };
var array2=new[] { 5, 6, 7 };

if(i>0)
    array1[1]=4;
else
    array2[2]=4;

Der obige Code wird jedoch kompiliert. Ersetzen Sie die if-else-Anweisung durch den bedingten Operator.

var array1=new[] { 1, 2, 3 };
var array2=new[] { 5, 6, 7 };
(i>0?array1[1]:array2[2])=4; // incorrect usage 

Der bedingte Operator und die if-else-Anweisungen sind konzeptionell gleich, wenn Sie dasselbe tun. Mit dem bedingten Operator in C ist dies möglicherweise sogar noch schneller , da C näher an der Baugruppe der Plattform liegt.


Für den ursprünglichen Code, den Sie angegeben haben, wird der bedingte Operator in einer foreach-Schleife verwendet, die die Dinge durcheinander bringt, um den Unterschied zwischen ihnen zu erkennen. Also schlage ich folgenden Code vor:

public static class TestClass {
    public static void TestConditionalOperator(int i) {
        long value=0;
        value+=i>0?2:3;
    }

    public static void TestIfElse(int i) {
        long value=0;

        if(i>0) {
            value+=2;
        }
        else {
            value+=3;
        }
    }

    public static void TestMethod() {
        TestConditionalOperator(0);
        TestIfElse(0);
    }
}

und das Folgende sind zwei Versionen der IL von optimiert und nicht. Da sie lang sind, verwende ich ein Bild, um zu zeigen, dass die rechte Seite die optimierte ist:

(Klicken Sie hier, um das Bild in voller Größe anzuzeigen.) hSN6s.png

In beiden Codeversionen sieht die IL des bedingten Operators kürzer aus als die if-else-Anweisung, und es besteht immer noch ein Zweifel daran, dass der Maschinencode endgültig generiert wurde. Das Folgende sind die Anweisungen beider Methoden, und das erstere Bild ist nicht optimiert, das letztere ist das optimierte:

  • Nicht optimierte Anweisungen: (Klicken Sie hier, um das Bild in voller Größe anzuzeigen.) ybhgM.png

  • Optimierte Anweisungen: (Klicken Sie hier, um das Bild in voller Größe anzuzeigen.) 6kgzJ.png

In letzterem ist der gelbe Block der Code, der nur ausgeführt wird, wenn i<=0, und der blaue Block ist, wenn i>0. In beiden Versionen von Anweisungen ist die if-else-Anweisung kürzer.

Beachten Sie, dass für verschiedene Anweisungen der [ CPI ] nicht unbedingt gleich ist. Für den identischen Befehl kosten logischerweise mehr Befehle einen längeren Zyklus. Wenn jedoch auch die Abrufzeit des Befehls und die Pipe / der Cache berücksichtigt wurden, hängt die tatsächliche Gesamtausführungszeit vom Prozessor ab. Der Prozessor kann auch die Zweige vorhersagen.

Moderne Prozessoren haben noch mehr Kerne, damit können die Dinge komplexer werden. Wenn Sie ein Intel-Prozessorbenutzer sind, sollten Sie sich [ Referenzhandbuch zur Optimierung von Intel® 64- und IA-32-Architekturen ] ansehen .

Ich weiß nicht, ob es eine Hardware-implementierte CLR gab, aber wenn ja, werden Sie mit dem bedingten Operator wahrscheinlich schneller, weil die IL offensichtlich geringer ist.

Hinweis: Der gesamte Maschinencode ist x86.

Ken Kin
quelle
7

Ich habe das getan, was Jon Skeet getan hat, und habe 1 Iteration und 1.000 Iterationen durchlaufen und sowohl von OP als auch von Jon ein anderes Ergebnis erhalten. Bei mir ist der Ternär nur etwas schneller. Unten ist der genaue Code:

static void runIfElse(int[] array, int iterations)
    {
        long value = 0;
        Stopwatch ifElse = new Stopwatch();
        ifElse.Start();
        for (int c = 0; c < iterations; c++)
        {
            foreach (int i in array)
            {
                if (i > 0)
                {
                    value += 2;
                }
                else
                {
                    value += 3;
                }
            }
        }
        ifElse.Stop();
        Console.WriteLine(String.Format("Elapsed time for If-Else: {0}", ifElse.Elapsed));
    }

    static void runTernary(int[] array, int iterations)
    {
        long value = 0;
        Stopwatch ternary = new Stopwatch();
        ternary.Start();
        for (int c = 0; c < iterations; c++)
        {
            foreach (int i in array)
            {
                value += i > 0 ? 2 : 3;
            }
        }
        ternary.Stop();


        Console.WriteLine(String.Format("Elapsed time for Ternary: {0}", ternary.Elapsed));
    }

    static void Main(string[] args)
    {
        Random r = new Random();
        int[] array = new int[20000000];
        for (int i = 0; i < array.Length; i++)
        {
            array[i] = r.Next(int.MinValue, int.MaxValue);
        }
        Array.Sort(array);

        long value = 0;

        runIfElse(array, 1);
        runTernary(array, 1);
        runIfElse(array, 1000);
        runTernary(array, 1000);
        
        Console.ReadLine();
    }

Die Ausgabe von meinem Programm:

Verstrichene Zeit für If-Else: 00: 00: 00.0140543

Verstrichene Zeit für Ternary: 00: 00: 00.0136723

Verstrichene Zeit für If-Else: 00: 00: 14.0167870

Verstrichene Zeit für Ternary: 00: 00: 13.9418520

Ein weiterer Lauf in Millisekunden:

Verstrichene Zeit für If-Else: 20

Verstrichene Zeit für Ternary: 19

Verstrichene Zeit für If-Else: 13854

Verstrichene Zeit für Ternary: 13610

Dies läuft in 64-Bit-XP, und ich lief ohne Debugging.

Bearbeiten - Wird in x86 ausgeführt:

Es gibt einen großen Unterschied bei der Verwendung von x86. Dies geschah ohne Debugging auf und auf demselben xp 64-Bit-Computer wie zuvor, wurde jedoch für x86-CPUs entwickelt. Das sieht eher nach OPs aus.

Verstrichene Zeit für If-Else: 18

Verstrichene Zeit für Ternary: 35

Verstrichene Zeit für If-Else: 20512

Verstrichene Zeit für Ternary: 32673

Shaz
quelle
Könnten Sie es bitte auf x86 versuchen? Vielen Dank.
user1032613
@ user1032613 Ich denke, es kann einen großen Unterschied geben, wenn Sie ohne Debugging oder mit Debugging ausführen.
CodeCamper
@ user1032613 Ich habe gerade meinen Beitrag mit Daten von x86 bearbeitet. Es sieht eher wie deins aus, wo ternär 2x langsamer ist.
Shaz
5

Der generierte Assembler-Code erzählt die Geschichte:

a = (b > c) ? 1 : 0;

Erzeugt:

mov  edx, DWORD PTR a[rip]
mov  eax, DWORD PTR b[rip]
cmp  edx, eax
setg al

Wohingegen:

if (a > b) printf("a");
else printf("b");

Erzeugt:

mov edx, DWORD PTR a[rip]
mov eax, DWORD PTR b[rip]
cmp edx, eax
jle .L4
    ;printf a
jmp .L5
.L4:
    ;printf b
.L5:

Das Ternär kann also kürzer und schneller sein, einfach weil weniger Anweisungen und keine Sprünge verwendet werden, wenn Sie nach wahr / falsch suchen. Wenn Sie andere Werte als 1 und 0 verwenden, erhalten Sie denselben Code wie ein if / else, zum Beispiel:

a = (b > c) ? 2 : 3;

Erzeugt:

mov edx, DWORD PTR b[rip]
mov eax, DWORD PTR c[rip]
cmp edx, eax
jle .L6
    mov eax, 2
jmp .L7
.L6:
    mov eax, 3
.L7:

Welches ist das gleiche wie das if / else.


quelle
4

Ohne Debugging Strg + F5 ausführen, scheint der Debugger sowohl ifs als auch ternary erheblich zu verlangsamen, aber es scheint, dass er den ternären Operator viel mehr verlangsamt.

Wenn ich den folgenden Code ausführe, sind hier meine Ergebnisse. Ich denke, der kleine Millisekundenunterschied wird dadurch verursacht, dass der Compiler max = max optimiert und entfernt, aber diese Optimierung wahrscheinlich nicht für den ternären Operator vornimmt. Wenn jemand die Baugruppe überprüfen und bestätigen könnte, wäre das großartig.

--Run #1--
Type   | Milliseconds
Ternary 706
If     704
%: .9972
--Run #2--
Type   | Milliseconds
Ternary 707
If     704
%: .9958
--Run #3--
Type   | Milliseconds
Ternary 706
If     704
%: .9972

Code

  for (int t = 1; t != 10; t++)
        {
            var s = new System.Diagnostics.Stopwatch();
            var r = new Random(123456789);   //r
            int[] randomSet = new int[1000]; //a
            for (int i = 0; i < 1000; i++)   //n
                randomSet[i] = r.Next();     //dom
            long _ternary = 0; //store
            long _if = 0;      //time
            int max = 0; //result
            s.Start();
            for (int q = 0; q < 1000000; q++)
            {
                for (int i = 0; i < 1000; i++)
                    max = max > randomSet[i] ? max : randomSet[i];
            }
            s.Stop();
            _ternary = s.ElapsedMilliseconds;
            max = 0;
            s = new System.Diagnostics.Stopwatch();
            s.Start();
            for (int q = 0; q < 1000000; q++)
            {
                for (int i = 0; i < 1000; i++)
                    if (max > randomSet[i])
                        max = max; // I think the compiler may remove this but not for the ternary causing the speed difference.
                    else
                        max = randomSet[i];
            }

            s.Stop();
            _if = s.ElapsedMilliseconds;
            Console.WriteLine("--Run #" + t+"--");
            Console.WriteLine("Type   | Milliseconds\nTernary {0}\nIf     {1}\n%: {2}", _ternary, _if,((decimal)_if/(decimal)_ternary).ToString("#.####"));
        }
CodeCamper
quelle
4

Betrachtet man die generierte IL, so sind darin 16 Operationen weniger als in der if / else-Anweisung (Kopieren und Einfügen des Codes von @ JonSkeet). Dies bedeutet jedoch nicht, dass es ein schnellerer Prozess sein sollte!

Um die Unterschiede in IL zusammenzufassen, entspricht die if / else-Methode fast dem Lesen des C # -Codes (Durchführen der Addition innerhalb des Zweigs), während der bedingte Code entweder 2 oder 3 auf den Stapel lädt (abhängig vom Wert) und addiert es dann zu einem Wert außerhalb der Bedingung.

Der andere Unterschied ist die verwendete Verzweigungsanweisung. Die if / else-Methode verwendet einen brtrue (branch if true), um über die erste Bedingung zu springen, und einen bedingungslosen Zweig, um von der ersten aus der if-Anweisung zu springen. Der bedingte Code verwendet ein bgt (Verzweigung, wenn größer als) anstelle eines brtrue, was möglicherweise ein langsamerer Vergleich sein könnte.

Außerdem (nachdem ich gerade über die Verzweigungsvorhersage gelesen habe) kann es zu einer Leistungseinbuße kommen, wenn die Verzweigung kleiner ist. Der bedingte Zweig hat nur 1 Befehl innerhalb des Zweigs, der if / else hat jedoch 7. Dies würde auch erklären, warum es einen Unterschied zwischen der Verwendung von long und int gibt, da das Ändern eines int die Anzahl der Befehle in den if / else-Zweigen um 1 verringert (macht das Vorlesen weniger)

Matthew Steeples
quelle
1

Im folgenden Code scheint if / else ungefähr 1,4-mal schneller zu sein als der ternäre Operator. Ich fand jedoch heraus, dass die Einführung einer temporären Variablen die Laufzeit des ternären Operators ungefähr um das 1,4-fache verringert:

If / Else: 98 ms

Ternär: 141 ms

Ternär mit Temp Var: 100 ms

using System;
using System.Diagnostics;

namespace ConsoleApplicationTestIfElseVsTernaryOperator
{
    class Program
    {
        static void Main(string[] args)
        {
            Random r = new Random(0);
            int[] array = new int[20000000];
            for (int i = 0; i < array.Length; i++)
            {
                array[i] = r.Next(int.MinValue, int.MaxValue);
            }
            Array.Sort(array);
            long value;
            Stopwatch stopwatch = new Stopwatch();

            value = 0;
            stopwatch.Restart();
            foreach (int i in array)
            {
                if (i > 0)
                {
                    value += 2;
                }
                else
                {
                    value += 3;
                }
                // 98 ms
            }
            stopwatch.Stop();
            Console.WriteLine("If/Else: " + stopwatch.ElapsedMilliseconds.ToString() + " ms");

            value = 0;
            stopwatch.Restart();
            foreach (int i in array)
            {
                value += (i > 0) ? 2 : 3; 
                // 141 ms
            }

            stopwatch.Stop();
            Console.WriteLine("Ternary: " + stopwatch.ElapsedMilliseconds.ToString() + " ms");

            value = 0;
            int tempVar = 0;
            stopwatch.Restart();
            foreach (int i in array)
            {
                tempVar = (i > 0) ? 2 : 3;
                value += tempVar; 
                // 100ms
            }
            stopwatch.Stop();
            Console.WriteLine("Ternary with temp var: " + stopwatch.ElapsedMilliseconds.ToString() + " ms");

            Console.ReadKey(true);
        }
    }
}
Alexey Novikov
quelle
0

Zu viele gute Antworten, aber ich fand etwas Interessantes, sehr einfache Änderungen wirken sich aus. Nachdem Sie die folgenden Änderungen vorgenommen haben, dauert es dieselbe Zeit, um if-else und den ternären Operator auszuführen.

anstatt unter die Zeile zu schreiben

value +=  i > 0 ? 2 : 3;

Ich habe das benutzt,

int a =  i > 0 ? 2 : 3;
value += a;

Eine der folgenden Antworten erwähnt auch, dass es eine schlechte Art ist, einen ternären Operator zu schreiben.

Ich hoffe, dies wird Ihnen helfen, einen ternären Operator zu schreiben, anstatt zu überlegen, welcher besser ist.

Verschachtelter ternärer Operator: Ich habe einen verschachtelten ternären Operator gefunden und mehrere if else-Blöcke benötigen ebenfalls dieselbe Zeit für die Ausführung.

Ravindra Sinare
quelle