Optimización de la evaluación de condiciones

by Valeriano Tortola 6. octubre 2007 02:51

En C#, el orden de evaluación de operadores es de izquierda a derecha por orden de prioridad en cualquier tipo de operación. Además, a la hora de evaluar condiciones lógicas se puede anticipar el resultado final si el resultado parcial hasta el momento es inamovible, es decir, que pase lo que pase en el resto de condiciones el resultado parcial será el definitivo, en ese caso, el resto de condiciones no son evaluadas.

Desde el punto de vista del rendimiento, nos permite ahorrar evaluaciones (y con ello instrucciones) innecesarias en nuestra lógica si nos aprovechamos bien de esta característica.

Para ejemplificar este comportamiento, observa este simple programa:

static void Main(string[] args)
{
  if (Test1(false) || Test2(false) || Test3(false))
  {
    Console.WriteLine("Dentro!");
  }
 
  Console.ReadKey();
}
 
static Boolean Test1(Boolean param)
{
  Console.WriteLine("Test1");
  return param ^ true;
}
 
static Boolean Test2(Boolean param)
{
  Console.WriteLine("Test2");
  return param ^ true;
}
 
static Boolean Test3(Boolean param)
{
  Console.WriteLine("Test3");
  return param ^ true;
}
}

La instrucción 'if' evalua una condición formada por dos operaciones OR sobre tres métodos que devuelven true ó false. Al devolver 'Test1' un true y siendo una opearción OR sea cual sea el resultado de 'Test2' y 'Test3' ... el resultado final será true, por lo que estos dos últimos no son evaluados. El programa devuelve la siguiente salida:

Test1
Dentro!

Sin embargo, si modificamos la línea del 'if' por:

if (Test1(true) || Test2(false) || Test3(false))

Test1
Test2
Dentro!

El resultado varia, ya que al devolver 'Test1' false, el resultado final no se puede anticipar, pero cuando 'Test2' devuelve true, ya no es necesario evaluar 'Test3':

Por ejemplo si sustituimos los OR por AND, el resultado es distinto ... pero siguiendo la misma línea de actuación:

  if (Test1(false) && Test2(false) && Test3(false))

Test1
Test2
Test3
Dentro!

Al ser las dos primeras condiciones true, cualquier resultado parcial podría ser anulado por un false en 'Test3', por lo que debe evaluar las 3ª.

Este comportamiento aplica a cualquier tipo de operación booleana, por compleja que sea, si en un determinado momento dada la ecuación lógica, se sabe que el resultado parcial será definitivo... el resto se obvia. 

La última sentencia 'if' probada, podría ser fácilmente substituible por condiciones anidadas ya que al igual que la operación con AND, evaluar una condición implica que se ha superado la anterior:

if (Test1(false))
{
  if (Test2(false))
  {
    if (Test3(false))
    {
      Console.WriteLine("Dentro!");
    }
  }
}

Pero en el resto de operaciones implica estructuras más complejas, que al igual que esta última (por simple que sea) deben ser evitadas siempre que no haya que emprender aluguna acción por cada condición a evaluar ó sea un requerimiento evaluarlas todas.

Las estructuras condicionales desmesuradas y mal estructuradas, no solo implican un código ilegible y niveles de identación inaceptables, si no que una mala estructuración puede derivar en la evaluación redundante e innecesaria de condiciones. Evaluar una condición simple como el valor de una variable puede ser algo insignificante, pero si la evaluación es directamente resultado de un método... ejecutar dicho método ó no puede significar una diferencia de rendimiento proporcional al tiempo de CPU que lleva ejecutarlo. Evita la lógica innecesaria !!

Una vez tengamos claro en flujo lógico de nuestra aplicación, y si como decía las condiciones se evaluan pero no se hace nada por cada una de ellas, podemos simplificar fácilmente pequeños grupos de evaluaciones mediante las tablas de Karnaugh, y ecuaciones ya más grandes mediante el algrebra de Boole, muy muy útil para simplificar bloques de lógica de negocios. Dos cosas de las que escribiré en mi blog en breve...

Como colofón, cuando evaluemos condiciones que pueden tomar muchos valores, mejor que la sucesión continua de else if, es mejor usar la instrucción switch. Como muestra un sencillo benchmark (que llevaba tiempo sin hacer uno xD):

Stopwatch sw = new Stopwatch();
 
sw.Start();
for(int i =0;i<1000000;i++)
  TestCharIfElse('z');
sw.Stop();
 
Console.WriteLine("TestCharIfElse: {0} ms.",sw.ElapsedMilliseconds);
sw.Reset();
 
sw.Start();
for (int i = 0; i < 1000000; i++)
  TestCharSwitch('z');
sw.Stop();
 
Console.WriteLine("TestCharSwitch: {0} ms.", sw.ElapsedMilliseconds);

Por un lado, 'TestCharIfElse' que com prueba si un caracter está en el alfabeto por medio de instruciones 'if else':

static Boolean TestCharIfElse(Char c)
{
  if (c == 'a')
  {
    return true;
  }
  else if (c == 'b')
  {
    return true;
  }
  
    [... Omitido por brevedad...]
 
  else if (c == 'z')
  {
    return true;
  }
  else return false;
}

Por otro, 'TestCharSwitch', que realiza la misma comprobación pero con la instrucción switch:

static Boolean TestCharSwitch(Char c)
{
  switch (c)
  {
    case 'a': return true;
    case 'b': return true;
    [... Omitido por brevedad ...]
    case 'z': return true;
    default: return false;
 
  }
}

La prueba envia un millón de veces el caracter 'z', que obliga a 'TestCharElseIf' a recorrer todas sus condiciones, mientras que 'TestCharSwitch' lo encuentra de forma más rápida como muestra el resultado:

TestCharIfElse: 57 ms.
TestCharSwitch: 12 ms.

Casi 5 veces más rápido en este caso.

Tags: ,

.NET 2.0 | C# 2.0

Comentarios

06/10/2007 4:18:35 #

trackback

Trackback from vtortola

Optimización de la evaluación de condiciones

vtortola |

Comentarios no permitidos