27 Stimmen

Ist der bedingte Operator langsam?

Ich sah mir einen Code mit einer riesigen switch-Anweisung und einer if-else-Anweisung für jeden Fall an und verspürte sofort den Drang zu optimieren. Wie es sich für einen guten Entwickler gehört, machte ich mich auf den Weg, um ein paar harte Timing-Fakten zu erhalten und begann mit drei Varianten:

  1. Der ursprüngliche Code sieht wie folgt aus:

    public static bool SwitchIfElse(Key inKey, out char key, bool shift)
    {
        switch (inKey)
        {
           case Key.A: if (shift) { key = 'A'; } else { key = 'a'; } return true;
           case Key.B: if (shift) { key = 'B'; } else { key = 'b'; } return true;
           case Key.C: if (shift) { key = 'C'; } else { key = 'c'; } return true;
           ...
           case Key.Y: if (shift) { key = 'Y'; } else { key = 'y'; } return true;
           case Key.Z: if (shift) { key = 'Z'; } else { key = 'z'; } return true;
           ...
           //some more cases with special keys...
        }
        key = (char)0;
        return false;
    }
  2. Die zweite Variante ist die Verwendung des bedingten Operators:

    public static bool SwitchConditionalOperator(Key inKey, out char key, bool shift)
    {
        switch (inKey)
        {
           case Key.A: key = shift ? 'A' : 'a'; return true;
           case Key.B: key = shift ? 'B' : 'b'; return true;
           case Key.C: key = shift ? 'C' : 'c'; return true;
           ...
           case Key.Y: key = shift ? 'Y' : 'y'; return true;
           case Key.Z: key = shift ? 'Z' : 'z'; return true;
           ...
           //some more cases with special keys...
        }
        key = (char)0;
        return false;
    }
  3. Ein Twist, bei dem ein mit Schlüssel/Zeichen-Paaren vorgefülltes Wörterbuch verwendet wird:

    public static bool DictionaryLookup(Key inKey, out char key, bool shift)
    {
        key = '\0';
        if (shift)
            return _upperKeys.TryGetValue(inKey, out key);
        else
            return _lowerKeys.TryGetValue(inKey, out key);
    }

Hinweis: Die beiden switch-Anweisungen haben genau die gleichen Fälle und die Wörterbücher haben die gleiche Anzahl von Zeichen.

Ich hatte erwartet, dass 1) und 2) eine ähnliche Leistung erbringen und dass 3) etwas langsamer ist.

Für jede Methode, die zweimal 10.000.000 Iterationen zum Aufwärmen durchläuft und dann gemessen wird, erhalte ich zu meinem Erstaunen die folgenden Ergebnisse:

  1. 0,0000166 Millisekunden pro Anruf
  2. 0,0000779 Millisekunden pro Anruf
  3. 0,0000413 Millisekunden pro Anruf

Wie kann das sein? Der Bedingungsoperator ist viermal langsamer als if-else-Anweisungen und fast zweimal langsamer als Wörterbuchabfragen. Übersehe ich hier etwas Wesentliches oder ist der Bedingungsoperator von Natur aus langsam?

Aktualisierung 1: Ein paar Worte zu meinem Test-Kabelbaum. Ich lasse den folgenden (Pseudo-)Code für jede der oben genannten Varianten unter einer Freigabe kompiliertes .Net 3.5 Projekt in Visual Studio 2010. Die Code-Optimierung ist eingeschaltet und die DEBUG/TRACE-Konstanten sind deaktiviert. Ich führe die zu messende Methode einmal zum Aufwärmen aus, bevor ich einen zeitgesteuerten Lauf durchführe. Die Run-Methode führte die Methode für eine große Anzahl von Iterationen aus, mit shift sowohl auf "true" als auch auf "false" gesetzt und mit einem ausgewählten Satz von Eingabetasten:

Run(method);
var stopwatch = Stopwatch.StartNew();
Run(method);
stopwatch.Stop();
var measure = stopwatch.ElapsedMilliseconds / iterations;

Die Methode Run sieht folgendermaßen aus:

for (int i = 0; i < iterations / 4; i++)
{
    method(Key.Space, key, true);
    method(Key.A, key, true);
    method(Key.Space, key, false);
    method(Key.A, key, false);
}

Update 2: Ich habe mir die für 1) und 2) erstellten AWLs angesehen und festgestellt, dass die Hauptschalterstrukturen erwartungsgemäß identisch sind, die Gehäusekörper jedoch leichte Unterschiede aufweisen. Hier ist die IL, die ich mir ansehe:

1) If/else-Anweisung:

L_0167: ldarg.2 
L_0168: brfalse.s L_0170

L_016a: ldarg.1 
L_016b: ldc.i4.s 0x42
L_016d: stind.i2 
L_016e: br.s L_0174

L_0170: ldarg.1 
L_0171: ldc.i4.s 0x62
L_0173: stind.i2 

L_0174: ldc.i4.1 
L_0175: ret 

2) Der bedingte Operator:

L_0165: ldarg.1 
L_0166: ldarg.2 
L_0167: brtrue.s L_016d

L_0169: ldc.i4.s 0x62
L_016b: br.s L_016f

L_016d: ldc.i4.s 0x42
L_016f: stind.i2 

L_0170: ldc.i4.1 
L_0171: ret 

Einige Beobachtungen:

  • Der bedingte Operator verzweigt, wenn shift gleich wahr ist, während if/else-Verzweigungen, wenn shift falsch ist.
  • Während 1) tatsächlich zu ein paar mehr Anweisungen kompiliert wird als 2), ist die Anzahl der Anweisungen, die ausgeführt werden, wenn shift entweder wahr oder falsch ist, sind für beide gleich.
  • Die Befehlsreihenfolge für 1) ist so, dass immer nur ein Stack-Slot belegt ist, während 2) immer zwei lädt.

Bedeutet eine dieser Beobachtungen, dass der bedingte Operator langsamer arbeitet? Gibt es andere Nebeneffekte, die ins Spiel kommen?

13voto

Zyphrax Punkte 17659

Sehr merkwürdig, vielleicht geht die .NET-Optimierung in Ihrem Fall nach hinten los:

T Versionen von ternären Ausdrücken und stellte fest, dass sie identisch sind mit if-Anweisungen, mit einem kleinen Unterschied. Die ternäre Anweisung erzeugt manchmal Code, der die entgegengesetzte Bedingung testet, die Sie erwarten würde, z. B. testet sie, dass die Unterausdruck falsch ist, anstatt zu testen, ob er wahr ist. Dies führt zu einer Neuordnung einige der Anweisungen und kann gelegentlich die Leistung steigern.

http://dotnetperls.com/ternary

Sie möchten vielleicht die ToString auf den Enum-Wert (für die nicht-speziellen Fälle) zu betrachten:

string keyValue = inKey.ToString();
return shift ? keyValue : keyValue.ToLower();

EDITAR:
Ich habe die if-else-Methode mit dem ternären Operator verglichen, und bei 1000000 Zyklen ist der ternäre Operator immer mindestens so schnell wie die if-else-Methode (manchmal ein paar Millisekunden schneller, was den obigen Text bestätigt). Ich denke, dass Sie bei der Messung der benötigten Zeit einen Fehler gemacht haben.

11voto

jrista Punkte 31522

Ich wäre neugierig zu wissen, ob Sie dies mit einem Debug- oder Release-Build testen. Wenn es ein Debug-Build ist, dann könnte der Unterschied sehr wahrscheinlich ein Unterschied aufgrund der LACK von Low-Level-Optimierungen, die der Compiler hinzufügt, wenn Sie Release-Modus (oder manuell deaktivieren Debug-Modus und Compiler-Optimierungen zu aktivieren) verwenden.

Ich würde jedoch erwarten, dass bei eingeschalteter Optimierung der ternäre Operator entweder gleich schnell oder ein wenig schneller ist als die if/else-Anweisung, während die Wörterbuchsuche am langsamsten ist. Hier sind meine Ergebnisse, 10 Millionen Iterationen zum Aufwärmen, gefolgt von 10 Millionen zeitgesteuerten Iterationen, für jede:

DEBUG-MODUS

   If/Else: 00:00:00.7211259
   Ternary: 00:00:00.7923924
Dictionary: 00:00:02.3319567

FREIGABE-MODUS

   If/Else: 00:00:00.5217478
   Ternary: 00:00:00.5050474
Dictionary: 00:00:02.7389423

Interessant ist, dass die ternäre Berechnung vor der Aktivierung der Optimierungen langsamer war als die if/else-Berechnung, während sie danach schneller war.

EDITAR:

Nach weiteren Tests zeigt sich, dass es in der Praxis kaum einen Unterschied zwischen if/else und ternär gibt. Der ternäre Code führt zwar zu kleinerem AWL-Code, aber die Leistung der beiden ist ziemlich gleich. In einem Dutzend verschiedener Tests mit einer Binärdatei im Freigabemodus waren die Ergebnisse von if/else und ternary entweder identisch oder lagen bei 10.000.000 Iterationen nur um den Bruchteil einer Millisekunde auseinander. Manchmal war if/else etwas schneller, manchmal ternary, aber in der Praxis sind die Ergebnisse gleich.

Das Wörterbuch schneidet dagegen deutlich schlechter ab. Wenn es um diese Art von Optimierung geht, würde ich meine Zeit nicht damit verschwenden, zwischen if/else und ternary zu wählen, wenn der Code bereits existiert. Wenn Sie jedoch derzeit eine Dictionary-Implementierung haben, würde ich sie auf jeden Fall überarbeiten, um einen effizienteren Ansatz zu verwenden und Ihre Leistung um etwa 400 % zu verbessern (jedenfalls für die gegebene Funktion).

4voto

t0mm13b Punkte 33393

Interessant, ich bin losgezogen und habe eine kleine Klasse entwickelt IfElseTernaryTest hier, ok, der Code ist nicht wirklich "optimiert" oder ein gutes Beispiel, aber trotzdem...der Diskussion zuliebe:

public class IfElseTernaryTest
{
    private bool bigX;
    public void RunIfElse()
    {
        int x = 4; int y = 5;
        if (x &gt; y) bigX = false;
        else if (x &lt; y) bigX = true; 
    }
    public void RunTernary()
    {
        int x = 4; int y = 5;
        bigX = (x &gt; y) ? false : ((x &lt; y) ? true : false);
    }
}

Dies war der IL-Dump des Codes... der interessante Teil war, dass die ternären Anweisungen in IL tatsächlich kürzer waren als die if ....

.class /*02000003*/ public auto ansi beforefieldinit ConTern.IfElseTernaryTest
       extends [mscorlib/*23000001*/]System.Object/*01000001*/
{
  .field /*04000001*/ private bool bigX
  .method /*06000003*/ public hidebysig instance void 
          RunIfElse() cil managed
  // SIG: 20 00 01
  {
    // Method begins at RVA 0x205c
    // Code size       44 (0x2c)
    .maxstack  2
    .locals /*11000001*/ init ([0] int32 x,
             [1] int32 y,
             [2] bool CS$4$0000)
    .line 19,19 : 9,10 ''
//000013:     }
//000014: 
//000015:     public class IfElseTernaryTest
//000016:     {
//000017:         private bool bigX;
//000018:         public void RunIfElse()
//000019:         {
    IL_0000:  /* 00   |                  */ nop
    .line 20,20 : 13,23 ''
//000020:             int x = 4; int y = 5;
    IL_0001:  /* 1A   |                  */ ldc.i4.4
    IL_0002:  /* 0A   |                  */ stloc.0
    .line 20,20 : 24,34 ''
    IL_0003:  /* 1B   |                  */ ldc.i4.5
    IL_0004:  /* 0B   |                  */ stloc.1
    .line 21,21 : 13,23 ''
//000021:             if (x &gt; y) bigX = false;
    IL_0005:  /* 06   |                  */ ldloc.0
    IL_0006:  /* 07   |                  */ ldloc.1
    IL_0007:  /* FE02 |                  */ cgt
    IL_0009:  /* 16   |                  */ ldc.i4.0
    IL_000a:  /* FE01 |                  */ ceq
    IL_000c:  /* 0C   |                  */ stloc.2
    IL_000d:  /* 08   |                  */ ldloc.2
    IL_000e:  /* 2D   | 09               */ brtrue.s   IL_0019

    .line 21,21 : 24,37 ''
    IL_0010:  /* 02   |                  */ ldarg.0
    IL_0011:  /* 16   |                  */ ldc.i4.0
    IL_0012:  /* 7D   | (04)000001       */ stfld      bool ConTern.IfElseTernaryTest/*02000003*/::bigX /* 04000001 */
    IL_0017:  /* 2B   | 12               */ br.s       IL_002b

    .line 22,22 : 18,28 ''
//000022:             else if (x &lt; y) bigX = true; 
    IL_0019:  /* 06   |                  */ ldloc.0
    IL_001a:  /* 07   |                  */ ldloc.1
    IL_001b:  /* FE04 |                  */ clt
    IL_001d:  /* 16   |                  */ ldc.i4.0
    IL_001e:  /* FE01 |                  */ ceq
    IL_0020:  /* 0C   |                  */ stloc.2
    IL_0021:  /* 08   |                  */ ldloc.2
    IL_0022:  /* 2D   | 07               */ brtrue.s   IL_002b

    .line 22,22 : 29,41 ''
    IL_0024:  /* 02   |                  */ ldarg.0
    IL_0025:  /* 17   |                  */ ldc.i4.1
    IL_0026:  /* 7D   | (04)000001       */ stfld      bool ConTern.IfElseTernaryTest/*02000003*/::bigX /* 04000001 */
    .line 23,23 : 9,10 ''
//000023:         }
    IL_002b:  /* 2A   |                  */ ret
  } // end of method IfElseTernaryTest::RunIfElse

  .method /*06000004*/ public hidebysig instance void 
          RunTernary() cil managed
  // SIG: 20 00 01
  {
    // Method begins at RVA 0x2094
    // Code size       27 (0x1b)
    .maxstack  3
    .locals /*11000002*/ init ([0] int32 x,
             [1] int32 y)
    .line 25,25 : 9,10 ''
//000024:         public void RunTernary()
//000025:         {
    IL_0000:  /* 00   |                  */ nop
    .line 26,26 : 13,23 ''
//000026:             int x = 4; int y = 5;
    IL_0001:  /* 1A   |                  */ ldc.i4.4
    IL_0002:  /* 0A   |                  */ stloc.0
    .line 26,26 : 24,34 ''
    IL_0003:  /* 1B   |                  */ ldc.i4.5
    IL_0004:  /* 0B   |                  */ stloc.1
    .line 27,27 : 13,63 ''
//000027:             bigX = (x &gt; y) ? false : ((x &lt; y) ? true : false);
    IL_0005:  /* 02   |                  */ ldarg.0
    IL_0006:  /* 06   |                  */ ldloc.0
    IL_0007:  /* 07   |                  */ ldloc.1
    IL_0008:  /* 30   | 0A               */ bgt.s      IL_0014

    IL_000a:  /* 06   |                  */ ldloc.0
    IL_000b:  /* 07   |                  */ ldloc.1
    IL_000c:  /* 32   | 03               */ blt.s      IL_0011

    IL_000e:  /* 16   |                  */ ldc.i4.0
    IL_000f:  /* 2B   | 01               */ br.s       IL_0012

    IL_0011:  /* 17   |                  */ ldc.i4.1
    IL_0012:  /* 2B   | 01               */ br.s       IL_0015

    IL_0014:  /* 16   |                  */ ldc.i4.0
    IL_0015:  /* 7D   | (04)000001       */ stfld      bool ConTern.IfElseTernaryTest/*02000003*/::bigX /* 04000001 */
    .line 28,28 : 9,10 ''
//000028:         }
    IL_001a:  /* 2A   |                  */ ret
  } // end of method IfElseTernaryTest::RunTernary

Es scheint also, dass der ternäre Operator anscheinend kürzer und, wie ich vermute, schneller ist, da weniger Anweisungen verwendet werden... aber auf dieser Basis scheint es Ihrem Fall #2 zu widersprechen, was überraschend ist...

Edita: Nach dem Kommentar von Sky, der eine "Codeaufblähung für #2" andeutete, wird dies widerlegen, was Sky sagte!!! Ok, der Code ist anders, der Kontext ist anders, es ist eine Beispielübung, um den IL Dump zu überprüfen...

3voto

Doug Domeny Punkte 4340

Ich würde erwarten, dass Nr. 1 und Nr. 2 identisch sind. Der Optimierer sollte den gleichen Code ergeben. Das Wörterbuch in #3 würde erwartet, langsam zu sein, es sei denn, es ist irgendwie optimiert, um nicht tatsächlich einen Hash verwenden.

Bei der Programmierung von Echtzeitsystemen haben wir immer eine Nachschlagetabelle - ein einfaches Array - verwendet, um wie in Ihrem Beispiel zu übersetzen. Das ist am schnellsten, wenn der Bereich der Eingabe relativ klein ist.

2voto

Noon Silk Punkte 52750

Ich verstehe nicht ganz, warum Sie erwarten, dass eine if-Anweisung langsamer ist als eine Wörterbuchsuche. Zumindest muss ein Hashcode berechnet werden, der dann in einer Liste nachgeschlagen werden muss. Ich verstehe nicht, warum Sie annehmen, dass dies schneller ist als ein cmp/jmp.

Ich glaube nicht einmal, dass die Methode, die Sie optimieren, so gut ist; es scheint, dass sie in der Aufrufphase besser gemacht werden könnte (obwohl ich nicht sicher sein kann, da Sie den Kontext nicht angegeben haben).

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X