2009-05-19 14 views
24

Cualquier idea de por qué este código:¿Por qué vería ~ 20% de aumento de velocidad usando código nativo?

extern "C" __declspec(dllexport) void Transform(double x[], double y[], int iterations, bool forward) 
{ 
    long n, i, i1, j, k, i2, l, l1, l2; 
    double c1, c2, tx, ty, t1, t2, u1, u2, z; 

    /* Calculate the number of points */ 
    n = (long)pow((double)2, (double)iterations); 

    /* Do the bit reversal */ 
    i2 = n >> 1; 
    j = 0; 
    for (i = 0; i < n - 1; ++i) 
    { 
     if (i < j) 
     { 
      tx = x[i]; 
      ty = y[i]; 
      x[i] = x[j]; 
      y[i] = y[j]; 
      x[j] = tx; 
      y[j] = ty; 
     } 
     k = i2; 
     while (k <= j) 
     { 
      j -= k; 
      k >>= 1; 
     } 
     j += k; 
    } 

    /* Compute the FFT */ 
    c1 = -1.0; 
    c2 = 0.0; 
    l2 = 1; 
    for (l = 0; l < iterations; ++l) 
    { 
     l1 = l2; 
     l2 <<= 1; 
     u1 = 1; 
     u2 = 0; 
     for (j = 0; j < l1; j++) 
     { 
      for (i = j; i < n; i += l2) 
      { 
       i1 = i + l1; 
       t1 = u1 * x[i1] - u2 * y[i1]; 
       t2 = u1 * y[i1] + u2 * x[i1]; 
       x[i1] = x[i] - t1; 
       y[i1] = y[i] - t2; 
       x[i] += t1; 
       y[i] += t2; 
      } 
      z = u1 * c1 - u2 * c2; 
      u2 = u1 * c2 + u2 * c1; 
      u1 = z; 
     } 
     c2 = sqrt((1.0 - c1)/2.0); 
     if (forward) 
      c2 = -c2; 
     c1 = sqrt((1.0 + c1)/2.0); 
    } 

    /* Scaling for forward transform */ 
    if (forward) 
    { 
     for (i = 0; i < n; ++i) 
     { 
      x[i] /= n; 
      y[i] /= n; 
     } 
    } 
} 

se ejecuta un 20% más rápido que el código?

public static void Transform(DataSet data, Direction direction) 
{ 
    double[] x = data.Real; 
    double[] y = data.Imag; 
    data.Direction = direction; 
    data.ExtremeImag = 0.0; 
    data.ExtremeReal = 0.0; 
    data.IndexExtremeImag = 0; 
    data.IndexExtremeReal = 0; 

    long n, i, i1, j, k, i2, l, l1, l2; 
    double c1, c2, tx, ty, t1, t2, u1, u2, z; 

    /* Calculate the number of points */ 
    n = (long)Math.Pow(2, data.Iterations); 

    /* Do the bit reversal */ 
    i2 = n >> 1; 
    j = 0; 
    for (i = 0; i < n - 1; ++i) 
    { 
     if (i < j) 
     { 
      tx = x[i]; 
      ty = y[i]; 
      x[i] = x[j]; 
      y[i] = y[j]; 
      x[j] = tx; 
      y[j] = ty; 
     } 
     k = i2; 
     while (k <= j) 
     { 
      j -= k; 
      k >>= 1; 
     } 
     j += k; 
    } 

    /* Compute the FFT */ 
    c1 = -1.0; 
    c2 = 0.0; 
    l2 = 1; 
    for (l = 0; l < data.Iterations; ++l) 
    { 
     l1 = l2; 
     l2 <<= 1; 
     u1 = 1; 
     u2 = 0; 
     for (j = 0; j < l1; j++) 
     { 
      for (i = j; i < n; i += l2) 
      { 
       i1 = i + l1; 
       t1 = u1 * x[i1] - u2 * y[i1]; 
       t2 = u1 * y[i1] + u2 * x[i1]; 
       x[i1] = x[i] - t1; 
       y[i1] = y[i] - t2; 
       x[i] += t1; 
       y[i] += t2; 
      } 
      z = u1 * c1 - u2 * c2; 
      u2 = u1 * c2 + u2 * c1; 
      u1 = z; 
     } 
     c2 = Math.Sqrt((1.0 - c1)/2.0); 
     if (direction == Direction.Forward) 
      c2 = -c2; 
     c1 = Math.Sqrt((1.0 + c1)/2.0); 
    } 

    /* Scaling for forward transform */ 
    if (direction == Direction.Forward) 
    { 
     for (i = 0; i < n; ++i) 
     { 
      x[i] /= n; 
      y[i] /= n; 
      if (Math.Abs(x[i]) > data.ExtremeReal) 
      { 
       data.ExtremeReal = x[i]; 
       data.IndexExtremeReal = (int)i; 
      } 
      if (Math.Abs(y[i]) > data.ExtremeImag) 
      { 
       data.ExtremeImag = y[i]; 
       data.IndexExtremeImag = (int)i; 
      } 
     } 
    } 
} 

FFT http://www.rghware.com/fft.png

creé la caída de la CPU se ve en el centro de la gráfica mediante la selección del “nativo DLL FFT” en mi aplicación:

http://www.rghware.com/InstrumentTuner.zip (código fuente)

Creo que esto funcionará en la mayoría de las PC. Tendrá que tener DirectX instalado. Tuve algunos problemas al usar la configuración de captura para cierto hardware. Se suponía que la configuración de captura era configurable, pero el desarrollo de la aplicación se ha visto desviado por este hallazgo interesante.

De todos modos, ¿por qué estoy viendo un aumento del 20% en la velocidad con el código nativo? Esto parece ir en contra de algunas de las suposiciones que tenía previamente.

ACTUALIZACIÓN

Después de la conversión de la función a un método inseguro y fijación de la cuestión de largo/int. El nuevo método inseguro en realidad se ejecuta más rápido que el método nativo (muy bueno).

Profile http://www.rghware.com/profile.png

Es obvio que la matriz de comprobación ligados es la causa del 20% lento en el presente método FFT. Debido a su naturaleza, los bucles for en este método no se pueden optimizar.

Gracias a todos por la ayuda.

+0

He cargado la fuente de nuevo (esta vez con las bibliotecas de clase. –

+1

¿Cómo está haciendo la prueba de comparación de velocidad? ¿Se ejecuta en versión fuera de un depurador (importante) y se ejecuta varias veces (para asegurarse de que no tiene problemas de JIT)? –

+0

Lo estoy ejecutando en versión fuera del IDE. Estoy usando System.Diagnostics.Stopwatch para probar la velocidad de estas funciones. Puse los resultados en el formulario para poder verlos y alternar entre los botones de opción. La función se realiza básicamente de forma continua en intervalos de medio segundo en los datos que ingresan a la tarjeta de sonido. He realizado la prueba muchas, muchas veces. –

Respuesta

23

Al solo mirar este código, sospecho desde mi experiencia una ralentización bastante significativa que va desde C++ -> C#.

Un problema importante que enfrentará en un puerto ingenuo de una rutina como esta en C# es que C# va a agregar límites comprobando cada control de matriz aquí. Debido a que nunca está recorriendo las matrices de una manera que se optimice (see this question for details), casi todos los accesos a la matriz recibirán verificación de límites.

Además, este puerto está bastante cerca de una asignación de 1 a 1 de C. Si ejecuta esto a través de un buen generador de perfiles de .NET, probablemente encontrará algunos puntos geniales que se pueden optimizar para volver a esto. cerca de la velocidad de C++ con uno o dos ajustes (esa es casi siempre mi experiencia en rutinas portadoras como esta).

Si desea obtener esto a una velocidad casi idéntica, sin embargo, probablemente necesite convertir esto a código inseguro y usar la manipulación del puntero en lugar de establecer directamente las matrices. Esto eliminará todos los problemas de comprobación de límites y recuperará su velocidad.


Editar: Veo una gran diferencia más, que puede ser la razón por la que su código inseguro C# se está ejecutando más lento.

Salida this page about C# compared to C++, en particular:

"El tipo largo:. En C#, el tipo de largo es de 64 bits, mientras que en C++, que es de 32 bits"

Debe convertir la versión C# para usar int, no largo. En C#, long es un tipo de 64 bits. Esto puede tener un efecto profundo en la manipulación de su puntero, porque creo que está agregando inadvertidamente una conversión long-> int (con control de desbordamiento) en cada llamada de puntero.

Además, mientras lo hace, es posible que desee intentar envolver toda la función en un unchecked block. C++ no está haciendo la comprobación de desbordamiento que está recibiendo en C#.

+0

Rico Mariani sobre comprobación de límites: http://blogs.msdn.com/ricom/archive/2006/07/12/663642.aspx – VVS

+0

@David: Me encanta el blog de Rico Mariani, pero en este caso, no es una comparación justa . Estaba comparando la reducción de velocidad en una aplicación GUI típica, no una situación de crujido de números puros. Incluso entonces, estaba viendo una caída general del 3% desde la comprobación de límites de la matriz. Sin embargo, esta rutina es casi todo el acceso a una matriz, por lo que los otros cálculos no amortiguarán el número. Esperaría mucho más que la caída del 3% (.6% si elimina el 10% más bajo) –

+0

Convertí mi código en un método inseguro que utiliza punteros para manipular los datos (consulte mi última edición en el cuerpo de la pregunta .) Los resultados no fueron los que esperaba. ¿Hay algún problema con mi implementación insegura? Puedo publicar el nuevo código si es necesario. –

0

¿Usó un perfilador como AQTime para ver dónde está el cuello de la botella? A veces es algo trivial cuando se traduce código nativo a código administrado. Por otro lado, como el código administrado en algunos escenarios es más lento que el código nativo, es posible que desee probar el código no seguro.

3

Esto probablemente se deba a que el compilador JIT genera código que no es tan eficiente como el código generado por el compilador nativo.

Es probable que el siguiente paso sea diseñar el código si le preocupa la disminución del 20% en el rendimiento, o puede considerar el uso de una biblioteca optimizada.

1

Teniendo en cuenta que el código administrado limita la comprobación en el índice de cada acceso de matriz, lo que no hace el código no administrado, diría que la diferencia es menor de lo que esperaba.

Si cambia también las matrices a punteros en el código administrado (ya que eso es lo que realmente son en el código no administrado), esperaría que tengan el mismo rendimiento.

+0

tenga en cuenta que hay casos en que la comprobación de límites de la matriz puede optimizarse (como cuando el límite es explícitamente Lucas

3

El compilador nativo puede realizar optimizaciones mucho más profundas y pesadas que un compilador JIT, como vectorización, análisis interprocedimiento, etc. Y las FFT pueden obtener grandes aceleraciones con la vectorización.

1

Acabo de ejecutar el código que publicó con int en lugar de long y realmente no hizo la diferencia. Sé que otras personas han tenido mejor suerte con FFT in .NET, lo que demuestra que .NET puede alcanzar o superar la proformancia de C++ incluso con matemática FFT.

Así que mi respuesta, o el código del póster está más optimizado (para C) que el del enlace, o está menos optimizado para C# que el del artículo que he vinculado.

Realicé dos juegos de pruebas en dos máquinas con .NET 2.0. Una máquina tenía XPSP2 y tenía un solo procesador, 850 MHz Pentium III, con 512Mb de RAM. La otra máquina tenía la compilación 5321 de Vista y tenía un solo procesador, Mobile Pentium 4 a 2 GHz, con 1Gb de RAM. En cada caso, calculé el promedio de 100 cálculos de FFT por separado en 217 (131072) valores de datos. A partir de estos valores, calculé el error estándar a partir de la desviación estándar.

Los resultados se muestran en ms. Los resultados para la máquina Pentium III son:

Not Optimized Optimized For Space Optimized For Speed 
Unmanaged 92.88 ± 0.09 88.23 ± 0.09 68.48 ± 0.03 
Managed C++ 72.89 ± 0.03 72.26 ± 0.04 71.35 ± 0.06 
C++/CLI 73.00 ± 0.05 72.32 ± 0.03 71.44 ± 0.04 
C# Managed 72.21 ± 0.04 69.97 ± 0.08 

los resultados para el Mobile Pentium 4 son:

  Not Optimized Optimized For Space Optimized For Speed 
Unmanaged 45.2 ± 0.1 30.04 ± 0.04 23.06 ± 0.04 
Managed C++ 23.5 ± 0.1 23.17 ± 0.08 23.36 ± 0.07 
C++/CLI 23.5 ± 0.1 23.11 ± 0.07 23.80 ± 0.05 
C# Managed 23.7 ± 0.1 22.78 ± 0.03 
+0

Parece que no puedo duplicar esto: Promedio de código nativo: 0.007396 segundos +/- 0.000017 Promedio para el código administrado de C#: 0.008422 segundos +/- 0.000027 Todavía estoy obteniendo un aumento de velocidad de% 18-19 en mi máquina usando el código nativo (el suyo no es mío). Voy a poner en mi computadora portátil para ver si hay alguna diferencia allí. –

+0

Me pregunto qué mejoras en el diseño del sistema operativo y el procesador han tenido en esos números en general. – JasonRShaver

Cuestiones relacionadas