¿El operador condicional es lento?


Estaba mirando un código con una enorme sentencia switch y una sentencia if-else en cada caso e instantáneamente sentí la necesidad de optimizar. Como un buen desarrollador siempre debería hacerlo, me propuse obtener algunos datos de tiempo difíciles y comencé con tres variantes:

  1. El código original se ve así:

    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. La segunda variante convertida para usar el operador condicional:

    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. Un giro usando un diccionario prellenado con pares clave / carácter:

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

Nota: las dos sentencias switch tienen exactamente los mismos casos y los diccionarios tienen la misma cantidad de caracteres.

Esperaba que 1) y 2) fueran algo similares en rendimiento y que 3) fuera un poco más lento.

Para cada método que se ejecuta dos veces 10.000.000 iteraciones para el calentamiento y luego cronometrado, para mi sorpresa obtengo los siguientes resultados:

  1. 0.0000166 milisegundos por call
  2. 0.0000779 milisegundos por llamada
  3. 0.0000413 milisegundos por llamada

¿Cómo puede ser esto? El operador condicional es cuatro veces más lento que las sentencias if-else y casi dos veces más lento que las búsquedas en el diccionario. ¿Me falta algo esencial aquí o el operador condicional es inherentemente lento?

Actualización 1: Unas palabras sobre mi arnés de prueba. Corro el siguiente código (pseudo) para cada una de las variantes anteriores bajo un Release compilado el proyecto.Net 3.5 en Visual Studio 2010. La optimización del código está activada y las constantes de DEPURACIÓN/SEGUIMIENTO están desactivadas. Corro el método bajo medición una vez para el calentamiento antes de hacer una carrera cronometrada. El método run ejecutó el método para un gran número de iteraciones, con shift establecido en true y false y con un conjunto selecto de claves de entrada:

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

El método Run se ve así:

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

Actualización 2: Cavando más, he mirado el IL generado para 1) y 2) y encontrar que las estructuras del interruptor principal son idénticas como yo esperaría, sin embargo, los cuerpos de casos tienen ligeras diferencias. Aquí está la IL que estoy mirando:

1) Declaración If/else:

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) El Operador Condicional:

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 

Algunas observaciones:

  • El operador condicional ramifica cuando shift es igual a true mientras que if/else ramifica cuando shift es false.
  • Mientras que 1) en realidad compila a unas pocas instrucciones más que 2), el número de instrucciones ejecutadas cuando shift es verdadero o falso, son iguales para los dos.
  • La orden de instrucciones para 1) es tal que solo una ranura de pila está ocupada en todo momento, mientras que 2) siempre carga dos.

¿Alguna de estas observaciones implica que el operador condicional funcionará más lento? ¿Hay otros efectos secundarios que entren en juego?

Author: Peter Lillevold, 2010-02-14

8 answers

Muy extraño, tal vez la optimización de. NET es backfireing en su caso:

El autor desmontó varios versiones de expresiones ternarias y encontrado que son idénticos a if-declaraciones, con una pequeña diferencia. La declaración ternaria a veces produce código que prueba el condición opuesta que usted haría esperar, como en las pruebas que el la subexpresión es falsa en lugar de probando si es verdad. Esto reordena algunas de las instrucciones y puede de vez en cuando aumentar el rendimiento.

Http://dotnetperls.com/ternary

Podría considerar el toString en el valor de enumeración (para los casos no especiales):

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

EDITAR:
He comparado el método if-else con el operador ternario y con 1000000 ciclos el operador ternario siempre es al menos tan rápido como el método if-else (a veces unos pocos milisegundos más rápido, que admite el texto anterior). Creo que has cometido algún tipo de error en midiendo el tiempo que tomó.

 12
Author: Zyphrax,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-02-14 01:29:35

Me gustaría saber si está probando esto con una compilación de depuración o Publicación. Si es una compilación de depuración, entonces la diferencia podría ser una diferencia debido a la FALTA de optimizaciones de bajo nivel que el compilador agrega cuando usa el modo de liberación (o deshabilita manualmente el modo de depuración y habilita las optimizaciones del compilador).)

Esperaría con optimizaciones en, sin embargo, que el operador ternario es la misma velocidad o un poco más rápido que la sentencia if/else, mientras que el la búsqueda en el diccionario es más lenta. Aquí están mis resultados, 10 millones de iteraciones de calentamiento seguidas de 10 millones cronometradas, para cada una:

MODO DE DEPURACIÓN

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

MODO DE LIBERACIÓN

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

Creo que es interesante señalar aquí que antes de que se habilitaran las optimizaciones, la computación ternaria era más lenta que if/else, mientras que después, era más rápida.

EDITAR:

Después de un poco más de pruebas, en un sentido práctico, hay poca o ninguna diferencia entre if / else y ternario. Mientras que el código ternario resulta en IL más pequeño, realizan prácticamente lo mismo que los demás. En una docena de pruebas diferentes con un binario de modo de liberación, los resultados if / else y ternary fueron idénticos, o desactivados por una fracción de milisegundo para 10.000.000 iteraciones. A veces si / else era un poco más rápido, a veces ternary era, pero en toda la practicidad, realizan lo mismo.

El diccionario funciona significativamente peor, por otro lado. Cuando se trata de este tipo de optimizaciones, no perdería mi tiempo eligiendo entre if / else y ternary si el código ya existe. Sin embargo, si actualmente tiene una implementación de diccionario, definitivamente lo refactorizaría para usar un enfoque más eficiente y mejorar su rendimiento en un 400% (para la función dada, de todos modos.)

 11
Author: jrista,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-02-14 02:32:13

Interesante, me fui y desarrollé una pequeña clase IfElseTernaryTest aquí, ok, el código no es realmente 'optimizado' o un buen ejemplo, pero no obstante...por el bien de la discusión:

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

Este fue el volcado IL del código...la parte interesante fue que las instrucciones ternarias en IL era en realidad más corto que el 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

Por lo que parece, que el operador ternario es aparentemente más corto y supongo, más rápido como menos instrucciones se utiliza...pero sobre esa base, parece contradiga tu caso # 2 que es sorprendente...

Edit: Después del comentario de Sky, sugiriendo 'código hinchado para #2', esto refutará lo que dijo Sky!!! Ok, el Código es diferente, el contexto es diferente, es un ejercicio de ejemplo para comprobar el volcado IL para ver...

 4
Author: t0mm13b,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-11-17 14:18:33

Esperaría que #1 y #2 fueran lo mismo. El optimizador debe dar como resultado el mismo código. El diccionario en #3 esperaría ser lento, a menos que esté optimizado de alguna manera para no usar un hash.

Al codificar sistemas en tiempo real, siempre usamos una tabla de búsqueda a una matriz simple to para traducir como se indica en su ejemplo. Es el más rápido cuando el rango de entrada es bastante pequeño.

 3
Author: Doug Domeny,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-02-14 00:59:20

No entiendo por qué esperarías que una declaración if fuera más lenta que una búsqueda en el diccionario. Por lo menos un hashcode necesita ser calculado y luego necesita ser buscado en una lista. No veo por qué asumir que esto es más rápido que un cmp/jmp.

Específicamente, ni siquiera creo que el método que estás optimizando sea tan bueno; parece que podría mejorarse en la etapa de llamada (aunque no puedo estar seguro, ya que no has proporcionado el contexto).

 2
Author: Noon Silk,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-02-14 02:06:08

Asumiendo que estás preocupado por el rendimiento de ese método (y si no lo estás, ¿por qué molestarte en publicarlo?), debe considerar almacenar los valores char en una matriz y convertir los valores Key en un índice en la matriz.

 1
Author: Robert Rossney,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-02-14 18:44:24

No tengo VS a mano pero seguramente hay una forma simple incorporada de obtener la clave como un personaje? Algo así como un método toString para que pueda reemplazar ese monstruoso switch con esto:

if (shift)
  return inKey.toString().toUppercase();
else
  return inKey.toString().toLowercase();
 0
Author: DisgruntledGoat,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-02-17 13:28:03

Elegiría la tercera opción solo porque es más legible/mantenible. Apuesto a que este código no es el cuello de botella del rendimiento de su aplicación.

 -1
Author: Jader Dias,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-02-14 00:59:24