2012-06-26 28 views
6

Por favor, ayúdenme a identificar cuál de estos siguientes es el código más optimizado?Optimización del rendimiento de for-loop/switch-statement

for(int i=0;i<count;i++) 
{ 
    switch(way) 
    { 
     case 1: 
      doWork1(i); 
      break; 
     case 2: 
      doWork2(i); 
      break; 
     case 3: 
      doWork3(i); 
      break; 
    } 
} 

O

switch(way) 
{ 
    case 1: 
     for(int i=0;i<count;i++) 
     { 
      doWork1(i); 
     } 
     break; 
    case 2: 
     for(int i=0;i<count;i++) 
     { 
      doWork2(i); 
     } 
     break; 
    case 3: 
     for(int i=0;i<count;i++) 
     { 
      doWork3(i); 
     } 
     break; 
} 

En el primer caso, sucede que hay una sobrecarga de siempre comprobación de la condición caja de conmutación en cada iteración. En el segundo caso, la sobrecarga no está allí. Siento que el segundo caso es mucho mejor. Si alguien tiene alguna otra solución, ayúdenme a sugerirlo.

+0

intente esto: http://stackoverflow.com/questions/445067/if-vs-switch-speed – Shiham

+1

¿Por qué no [medida] (http://msdn.microsoft.com/en-us/library /system.diagnostics.stopwatch.aspx) it? –

+0

@TimSchmelter mi proyecto está en Silverlight, el cronómetro no está disponible aquí. – vaibhav

Respuesta

5

A switch en valores bajos, contiguos es insanely rápido - este tipo de salto tiene un manejo altamente optimizado. Francamente, lo que pregunte hará que no tenga ninguna diferencia en la gran mayoría de los casos - cualquier cosa en doWork2(i); va a inundar esto; diablos, la llamada virtual sí mismo podría pantano.

Si realmente, realmente, realmente importa (y me cuesta pensar en un escenario real aquí), entonces: mídelo. En cualquier escenario donde sea notable, entonces solo forma de medirlo será con su código real, exacto - no se pueden generalizar optimizaciones pico.

Así:

  1. no importa
  2. medida
  3. no importa
+0

¿Se refiere a la tabla de salto 'switch' (IL) solamente? Si no es así, considere '' forma '' una propiedad costosa de evaluar. ¿Existe alguna posibilidad de optimización del compilador, excepto para la tabla de salto? – fjdumont

2

Se podría hacer algo como:

Func(void, int> doWork; 
switch(way) 
{ 
    case 1: 
     doWork = doWork1; 
     break; 
    case 2: 
     doWork = doWork2; 
     break; 
    case 3: 
     doWork = doWork3; 
     break; 
} 
for (int i=0;i<count;i++) 
{ 
    doWork(i); 
} 

(escrito aquí, el código podría no bastante compilar, sólo para darle la idea ...)

+0

Lo comprobaré. parece que funcionará – vaibhav

0

El segundo método es más eficiente; debe completar el completo para loop independientemente. Pero en el primer método, repite innecesariamente el caso declaración cuenta veces.

+0

. Siento que también, es solo que si hubiera mucha más optimización que podría haber sucedido en el segundo código, hubiera sido mejor :) – vaibhav

+0

@vaibhav te refieres a la optimización del * rendimiento *, o la reducción de la redundancia en tu código (es decir, el principio DRY)? No veo cómo podría ser más eficiente, al menos con la información que ha proporcionado. – McGarnagle

+0

reduciendo la redundancia para el segundo código, para una mejor legibilidad, y quería saber si el código redundante obstaculiza el rendimiento o no – vaibhav

1

Se debe medir para ver si vale la pena para optimizar o no (I estoy muy seguro de que it's not). Personalmente prefiero el primero para la legibilidad y la concisión (menos código, menos propenso a errores, más "dry").

Aquí hay otro enfoque que es aún más concisa:

for(int i = 0; i < count; i++) 
{ 
    doAllWays(way, i); // let the method decide what to do next 
} 

Todos los "caminos" parecen estar releated, de lo contrario no aparecerían en la misma switch. Por lo tanto, tiene sentido agruparlos en un método primero que hace el switch.

2

Me hago preguntas a mí mismo para la optimización

  1. En primer lugar, qué tan grande es contar? ¿Es 1,2,10, 10000000000?
  2. ¿Qué potencia tendrá la máquina para ejecutar el código?
  3. ¿Se supone que debo escribir menos código?
  4. ¿Alguien va a leer este código después de que lo escriba? Si es así, ¿cómo es el profesional ?
  5. ¿Qué me falta? ¿Hora? Velocidad ? Algo más ?
  6. ¿Qué es way? ¿De dónde lo consigo? ¿Cuáles son las probabilidades de way que son 1 o 2 o 3?

Es obvio que el primer fragmento de código se aplicará a la parte del conmutador hasta que i alcance el recuento, pero ¿cuán grande es el conteo? Si no es un número muy grande, ¿no importará? Si es demasiado grande y se obtiene un tiempo de ejecución muy lento, entonces es inútil. Sin embargo, como dije antes, si quiere que sea fácil de leer y puede garantizar que el conteo sea pequeño, ¿por qué no usar el primero? Es mucho más legible que el segundo y hay menos código que es algo que me gusta.

Segundo fragmento, se ve feo pero se debe preferir si el conteo es un número enorme.

0

suponiendo que tiene un problema de rendimiento aquí (como interruptor está muy, muy rápido en la mayoría de los casos):

Si te molesta acerca de su sentencia switch, le sugiero aplicar refactorización aquí.

El interruptor puede sustituirse fácilmente por un Patrón de estrategia (ya que el valor conmutado no se cambia en los bucles for, no es necesario cambiarlo en absoluto).

El objetivo de optimización real son los de bucles, pero sin contexto, es difícil de decir qué se puede hacer al respecto.

Aquí hay más información sobre los interruptores de refactorización (por ejemplo al patrón Estrategia) CodeProject Article on refactoring switch

1

En realidad, puede ser algo más rápido a pesar de algunos de los comentarios aquí.

Vamos a probarlo en realidad:

using System; 
using System.Diagnostics; 

namespace Demo 
{ 
    class Program 
    { 
     static void Main(string[] args) 
     { 
      int count = 1000000000; 

      Stopwatch sw = Stopwatch.StartNew(); 

      for (int way = 1; way <= 3; ++way) 
       test1(count, way); 

      var elapsed1 = sw.Elapsed; 
      Console.WriteLine("test1() took " + elapsed1); 

      sw.Restart(); 

      for (int way = 1; way <= 3; ++way) 
       test2(count, way); 

      var elapsed2 = sw.Elapsed; 
      Console.WriteLine("test2() took " + elapsed2); 

      Console.WriteLine("test2() was {0:f1} times as fast.", + ((double)elapsed1.Ticks)/elapsed2.Ticks); 
     } 

     static void test1(int count, int way) 
     { 
      for (int i = 0; i < count; ++i) 
      { 
       switch (way) 
       { 
        case 1: doWork1(); break; 
        case 2: doWork2(); break; 
        case 3: doWork3(); break; 
       } 
      } 
     } 

     static void test2(int count, int way) 
     { 
      switch (way) 
      { 
       case 1: 
        for (int i = 0; i < count; ++i) 
         doWork1(); 
        break; 

       case 2: 
        for (int i = 0; i < count; ++i) 
         doWork2(); 
        break; 

       case 3: 
        for (int i = 0; i < count; ++i) 
         doWork3(); 
        break; 
      } 
     } 

     static void doWork1() 
     { 
     } 

     static void doWork2() 
     { 
     } 

     static void doWork3() 
     { 
     } 
    } 
} 

Ahora bien, esto es muy poco realista, ya que el DoWork() métodos no hacen nada. Sin embargo, nos dará un tiempo de referencia.

Los resultados me pasa por una versión de lanzamiento en mi sistema de Windows 7 x64 son:

test1() took 00:00:03.8041522 
test2() took 00:00:01.7916698 
test2() was 2.1 times as fast. 

Así se mueve el bucle en la sentencia switch hace que sea más del doble de rápido.

Ahora vamos a hacer un poco más realista añadiendo un poco de código en DoWork():

using System; 
using System.Diagnostics; 

namespace Demo 
{ 
    class Program 
    { 
     static void Main(string[] args) 
     { 
      int count = 1000000000; 

      Stopwatch sw = Stopwatch.StartNew(); 

      for (int way = 1; way <= 3; ++way) 
       test1(count, way); 

      var elapsed1 = sw.Elapsed; 
      Console.WriteLine("test1() took " + elapsed1); 

      sw.Restart(); 

      for (int way = 1; way <= 3; ++way) 
       test2(count, way); 

      var elapsed2 = sw.Elapsed; 
      Console.WriteLine("test2() took " + elapsed2); 

      Console.WriteLine("test2() was {0:f1} times as fast.", + ((double)elapsed1.Ticks)/elapsed2.Ticks); 
     } 

     static int test1(int count, int way) 
     { 
      int total1 = 0, total2 = 0, total3 = 0; 

      for (int i = 0; i < count; ++i) 
      { 
       switch (way) 
       { 
        case 1: doWork1(i, ref total1); break; 
        case 2: doWork2(i, ref total2); break; 
        case 3: doWork3(i, ref total3); break; 
       } 
      } 

      return total1 + total2 + total3; 
     } 

     static int test2(int count, int way) 
     { 
      int total1 = 0, total2 = 0, total3 = 0; 

      switch (way) 
      { 
       case 1: 
        for (int i = 0; i < count; ++i) 
         doWork1(i, ref total1); 
        break; 

       case 2: 
        for (int i = 0; i < count; ++i) 
         doWork2(i, ref total2); 
        break; 

       case 3: 
        for (int i = 0; i < count; ++i) 
         doWork3(i, ref total3); 
        break; 
      } 

      return total1 + total2 + total3; 
     } 

     static void doWork1(int n, ref int total) 
     { 
      total += n; 
     } 

     static void doWork2(int n, ref int total) 
     { 
      total += n; 
     } 

     static void doWork3(int n, ref int total) 
     { 
      total += n; 
     } 
    } 
} 

Ahora consigo estos resultados:

test1() took 00:00:03.9153776 
test2() took 00:00:05.3220507 
test2() was 0.7 times as fast. 

Ahora es más lento para poner el bucle en ¡el interruptor! Este resultado contrario a la intuición es típico de este tipo de cosas y demuestra por qué SIEMPRE debe realizar pruebas de tiempo cuando está tratando de optimizar el código. (Y la optimización de código como este suele ser algo que ni siquiera debería hacer a menos que tenga buenas razones para sospechar que hay un cuello de botella. Sería mejor que se tomara el tiempo limpiando su código;))

I Hice algunas otras pruebas, y para los métodos doWork() un poco más simples, el método test2() fue más rápido. Realmente depende mucho de lo que el compilador JIT pueda hacer con las optimizaciones.

NOTA: Creo que el motivo de las diferencias de velocidad para mi segundo código de prueba es que el compilador JIT puede optimizar las llamadas "ref" al alinear las llamadas a doWork() cuando no están en un bucle en test1(); mientras que para test2() no puede (por alguna razón).

Cuestiones relacionadas