2008-12-14 18 views
51

Estoy escribiendo una aplicación financiera en C# donde el rendimiento (es decir, la velocidad) es crítico. Debido a que es una aplicación financiera, tengo que usar el tipo de datos Decimal intensivamente.C# Decimal datatype performance

He optimizado el código tanto como pude con la ayuda de un generador de perfiles. Antes de usar Decimal, todo estaba hecho con el tipo de datos Double y la velocidad era varias veces más rápida. Sin embargo, Double no es una opción debido a su naturaleza binaria, lo que causa una gran cantidad de errores de precisión en el transcurso de múltiples operaciones.

¿Hay alguna biblioteca de decimales con la que pueda interactuar con C# que podría darme una mejora en el rendimiento con respecto al tipo de datos Decimal nativo en .NET?

Sobre la base de las respuestas que ya tengo, me di cuenta que no era lo suficientemente clara, así que aquí están algunos detalles adicionales:

  • La aplicación tiene que ser tan rápido como pueda ir (es decir, tan rápido como fue cuando usar Doble en lugar de Decimal sería un sueño). El doble fue aproximadamente 15 veces más rápido que el decimal, ya que las operaciones están basadas en hardware.
  • El hardware ya es de primera categoría (estoy usando un Dual-Xenon Quad-Core) y la aplicación utiliza subprocesos, por lo que la utilización de la CPU siempre es del 100% en la máquina. Además, la aplicación se ejecuta en modo de 64 bits, lo que le da una ventaja de rendimiento medible de más de 32 bits.
  • He optimizado más allá del punto de cordura (más de un mes y medio optimizando, créalo o no, ahora se necesitan aproximadamente 1/5000 de lo que se necesitó para hacer los mismos cálculos que usé como referencia inicialmente); Esta optimización involucraba todo: procesamiento de cadenas, E/S, acceso e índices a la base de datos, memoria, bucles, cambiando la forma en que se hacían algunas cosas, e incluso utilizando "cambiar" por "si" marcó una diferencia. El generador de perfiles ahora muestra claramente que el culpable de rendimiento restante está en los operadores de tipo de datos Decimal. Nada más está sumando una cantidad considerable de tiempo.
  • Tienes que creerme aquí: he ido lo más lejos que pude en el ámbito de C# .NET para optimizar la aplicación, y estoy realmente sorprendido por su rendimiento actual. Ahora estoy buscando una buena idea para mejorar el rendimiento del decimal a algo cercano al doble. Sé que es solo un sueño, pero solo quería comprobar que pensé en todo lo posible. :)

Gracias!

+3

Usted está a punto de realizar una optimización prematura, simplemente codifique la aplicación correctamente, luego modifíquela después del hecho – TravisO

+0

Ha realizado todas las optimizaciones de bajo nivel de C#. Puede haber algunas mejoras algorítmicas restantes (por ejemplo, hacer menos operaciones en los decimales). – Brian

+2

Las operaciones de la base de datos en una aplicación financiera sensible al tiempo suena mal ... –

Respuesta

7

El problema es básicamente que el doble/flotación son compatibles con el hardware, mientras que Decimal y similares no lo son. Es decir. debe elegir entre velocidad + precisión limitada y mayor precisión + peor rendimiento.

22

Dice que debe ser rápido, pero ¿tiene requisitos concretos de velocidad? Si no, puede optimizar el pasado más allá de la sensatez :)

Como acaba de sugerirle un amigo sentado a mi lado, ¿puede actualizar su hardware? Es probable que sea más barato que volver a escribir el código.

La opción más obvia es usar enteros en lugar de decimales, donde una "unidad" es algo así como "una milésima de un centavo" (o lo que quieras, entiendes la idea). Si eso es factible o no dependerá de las operaciones que está realizando en los valores decimales para empezar. Necesitarás ser muy cuidadoso al manejar esto - es fácil cometer errores (al menos si eres como yo).

¿El perfilador muestra puntos de acceso específicos en su aplicación que puede optimizar individualmente?Por ejemplo, si necesita hacer muchos cálculos en un área pequeña de código, puede convertir de formato decimal a entero, hacer los cálculos y luego convertir de nuevo. Eso podría mantener el API en términos de decimales para la mayor parte del código, lo que bien puede hacer que sea más fácil de mantener. Sin embargo, si no tiene hotspots pronunciados, eso puede no ser factible.

1 para perfilar y que nos dice que la velocidad es un requisito definido, por cierto :)

+1

@Downvoter: ¿Desea comentar? –

40

puede utilizar el tipo de datos de largo. Claro, no podrás almacenar fracciones allí, pero si codificas tu aplicación para almacenar centavos en lugar de libras, estarás bien. La precisión es del 100% para los tipos de datos largos, y a menos que trabaje con un gran número (use un tipo de 64 bits de longitud), estará bien.

Si no puede obligar a almacenar centavos, envuelva un número entero en una clase y utilícelo.

+0

Estoy de acuerdo. Usa una máquina de 64 bits y larga. Además, me vino a la mente. Creo que .NET tenía algunas herramientas para generar más códigos específicos de la máquina. Creo que se llamaba ngen. Tal vez hay un rendimiento que se puede obtener allí ... –

+1

Este es definitivamente el camino a seguir. Use long o int, y almacene centavos, o fracciones de centavos dependiendo de la precisión que requiera. Muchas operaciones ahora serán tan rápidas o más rápidas que el uso de dobles. – Chris

+1

Envolverlo en una clase incurrirá en gastos generales, e incluso una estructura ralentizará las cosas, debido a la sobrecarga del operador o a que se requieran llamadas de función, por lo que el almacenamiento sin procesar/largo es mejor para el rendimiento. – Chris

1

No puedo dar un comentario o votar todavía porque recién comencé a desbordar la pila. Mi comentario sobre alexsmart (publicado el 23 de diciembre de 2008 12:31) es que la expresión Round (n/precision, precision), donde n es int y precisiones largas, no hará lo que él piensa:

1) n/precision devolverá una división entera, es decir, ya estará redondeada, pero no podrá usar decimales. El comportamiento de redondeo también es diferente de Math.Round (...).

2) El código "retorno Math.Round (n/precisión, precisión) .ToString()" no compila debido a una ambigüedad entre Math.Round (doble, int) y Math.Round (decimal, En t). Tendrá que convertir a decimal (no doble ya que es una aplicación financiera) y, por lo tanto, también puede ir con decimal en primer lugar.

3) n/precisión, donde la precisión es 4 no truncar a cuatro decimales, pero se divide por 4. Por ejemplo, Math.round ((decimal) (1234567/4), 4) rendimientos 308641. (1.234.567/4 = 308641.75), mientras que lo que probablemente desee es obtener 1235000 (redondeado a una precisión de 4 dígitos desde el final de 567). Tenga en cuenta que Math.Round permite redondear a un punto fijo, no a una precisión fija.

Actualización: Puedo agregar comentarios ahora pero no hay suficiente espacio para poner este en el área de comentarios.

5

¿Qué pasa con MMX/SSE/SSE2?

creo que va a ayudar ... así que ... decimal es el tipo de datos de 128 bits y 128 bits SSE2 es también ... y se puede añadir, submarino, div, decimal mul de garrapata 1 CPU ...

puede escribir DLL para SSE2 usando VC++ y luego usar esa DLL en su aplicación

por ejemplo // se puede hacer algo como esto

VC++

#include <emmintrin.h> 
#include <tmmintrin.h> 

extern "C" DllExport __int32* sse2_add(__int32* arr1, __int32* arr2); 

extern "C" DllExport __int32* sse2_add(__int32* arr1, __int32* arr2) 
{ 
    __m128i mi1 = _mm_setr_epi32(arr1[0], arr1[1], arr1[2], arr1[3]); 
    __m128i mi2 = _mm_setr_epi32(arr2[0], arr2[1], arr2[2], arr2[3]); 

    __m128i mi3 = _mm_add_epi32(mi1, mi2); 
    __int32 rarr[4] = { mi3.m128i_i32[0], mi3.m128i_i32[1], mi3.m128i_i32[2], mi3.m128i_i32[3] }; 
    return rarr; 
} 

C#

[DllImport("sse2.dll")] 
private unsafe static extern int[] sse2_add(int[] arr1, int[] arr2); 

public unsafe static decimal addDec(decimal d1, decimal d2) 
{ 
    int[] arr1 = decimal.GetBits(d1); 
    int[] arr2 = decimal.GetBits(d2); 

    int[] resultArr = sse2_add(arr1, arr2); 

    return new decimal(resultArr); 
} 
2

Pregunta anterior, aunque sigue siendo muy válida.

Aquí hay algunos números para apoyar la idea de usar Long.

tiempo necesario para realizar adiciones 100'000'000

Long  231 mS 
Double 286 mS 
Decimal 2010 mS 

en pocas palabras, decimal es ~ 10 veces más lento que el Largo o doble.

Código:

Sub Main() 
    Const TESTS = 100000000 
    Dim sw As Stopwatch 

    Dim l As Long = 0 
    Dim a As Long = 123456 
    sw = Stopwatch.StartNew() 
    For x As Integer = 1 To TESTS 
     l += a 
    Next 
    Console.WriteLine(String.Format("Long {0} mS", sw.ElapsedMilliseconds)) 

    Dim d As Double = 0 
    Dim b As Double = 123456 
    sw = Stopwatch.StartNew() 
    For x As Integer = 1 To TESTS 
     d += b 
    Next 
    Console.WriteLine(String.Format("Double {0} mS", sw.ElapsedMilliseconds)) 

    Dim m As Decimal = 0 
    Dim c As Decimal = 123456 
    sw = Stopwatch.StartNew() 
    For x As Integer = 1 To TESTS 
     m += c 
    Next 
    Console.WriteLine(String.Format("Decimal {0} mS", sw.ElapsedMilliseconds)) 

    Console.WriteLine("Press a key") 
    Console.ReadKey() 
End Sub 
+0

2 segundos para hacer ** 100 millones de ** adiciones. ¿De qué manera esto "apoya la idea de usar' long' "? Puede ser más lento, pero no se puede simplemente comparar velocidades, 50 millones de adiciones por segundo simplemente no vale la pena optimizar. – weston

+1

231 * mili * segundos, no * segundos *. No es más lento, es más rápido, que es lo que busca el OP. – smirkingman

+0

Hablando de 2010 milisegundos para el decimal. En 2 segundos hace 100 millones. Ese es un argumento que OP debería buscar en otros lugares para los cuellos de botella. No es un argumento que deberían volver a escribir para usar 'long', sin importar cuánto más rápido' long' sea. – weston

0

tienda "centavos" por medio de matrimonio. además de analizar las salidas de entrada e impresión, tiene la misma velocidad que midió. superas el límite del entero de 64 bits. tienes una división que no trunca. nota: depende de usted cómo usar el doble resultado después de las divisiones. esto me parece el enfoque más simple para sus requisitos.

2

La pregunta es bien discutida, pero desde que estuve cavando este problema por un tiempo me gustaría compartir algunos de mis resultados.

Definición del problema: Se sabe que los decimales son mucho más lentos que los dobles, pero las aplicaciones financieras no pueden tolerar ningún artefacto que surja cuando los cálculos se realizan en dobles.

Investigación

Mi objetivo fue medir diferentes enfoques de almacenamiento de números flotantes-señalar y hacer una conclusión a la que uno debe usarse para nuestra aplicación.

Si aceptamos usar Int64 para almacenar números de punto flotante con precisión fija. El multiplicador de 10^6 nos daba los dos: suficientes dígitos para almacenar fracciones y un gran rango para almacenar grandes cantidades. Por supuesto, hay que tener cuidado con este enfoque (las operaciones de multiplicación y división pueden ser complicadas), pero estábamos listos y queríamos medir este enfoque también. Una cosa que debes tener en cuenta, salvo los posibles errores de cálculo y desbordamientos, es que generalmente no puedes exponer esos números largos a la API pública. Por lo tanto, todos los cálculos internos podrían realizarse con largos, pero antes de enviar los números al usuario, deberían convertirse en algo más amigable.

Implementé una clase de prototipo simple que ajusta un valor largo a una estructura decimal (lo llamó Money) y lo agregué a las mediciones.

public struct Money : IComparable 
{ 
    private readonly long _value; 

    public const long Multiplier = 1000000; 
    private const decimal ReverseMultiplier = 0.000001m; 

    public Money(long value) 
    { 
     _value = value; 
    } 

    public static explicit operator Money(decimal d) 
    { 
     return new Money(Decimal.ToInt64(d * Multiplier)); 
    } 

    public static implicit operator decimal (Money m) 
    { 
     return m._value * ReverseMultiplier; 
    } 

    public static explicit operator Money(double d) 
    { 
     return new Money(Convert.ToInt64(d * Multiplier)); 
    } 

    public static explicit operator double (Money m) 
    { 
     return Convert.ToDouble(m._value * ReverseMultiplier); 
    } 

    public static bool operator ==(Money m1, Money m2) 
    { 
     return m1._value == m2._value; 
    } 

    public static bool operator !=(Money m1, Money m2) 
    { 
     return m1._value != m2._value; 
    } 

    public static Money operator +(Money d1, Money d2) 
    { 
     return new Money(d1._value + d2._value); 
    } 

    public static Money operator -(Money d1, Money d2) 
    { 
     return new Money(d1._value - d2._value); 
    } 

    public static Money operator *(Money d1, Money d2) 
    { 
     return new Money(d1._value * d2._value/Multiplier); 
    } 

    public static Money operator /(Money d1, Money d2) 
    { 
     return new Money(d1._value/d2._value * Multiplier); 
    } 

    public static bool operator <(Money d1, Money d2) 
    { 
     return d1._value < d2._value; 
    } 

    public static bool operator <=(Money d1, Money d2) 
    { 
     return d1._value <= d2._value; 
    } 

    public static bool operator >(Money d1, Money d2) 
    { 
     return d1._value > d2._value; 
    } 

    public static bool operator >=(Money d1, Money d2) 
    { 
     return d1._value >= d2._value; 
    } 

    public override bool Equals(object o) 
    { 
     if (!(o is Money)) 
      return false; 

     return this == (Money)o; 
    } 

    public override int GetHashCode() 
    { 
     return _value.GetHashCode(); 
    } 

    public int CompareTo(object obj) 
    { 
     if (obj == null) 
      return 1; 

     if (!(obj is Money)) 
      throw new ArgumentException("Cannot compare money."); 

     Money other = (Money)obj; 
     return _value.CompareTo(other._value); 
    } 

    public override string ToString() 
    { 
     return ((decimal) this).ToString(CultureInfo.InvariantCulture); 
    } 
} 

Experimento

Medí operaciones siguientes: suma, resta, multiplicación, división, comparación de igualdad y (mayor/menor) comparación relativa. Estaba midiendo operaciones en los siguientes tipos: double, long, decimal y Money. Cada operación se realizó 1.000.000 de veces. Todos los números se asignaron previamente en matrices, por lo que llamar al código personalizado en los constructores de decimal y Money no debería afectar los resultados.

Added moneys in 5.445 ms 
Added decimals in 26.23 ms 
Added doubles in 2.3925 ms 
Added longs in 1.6494 ms 

Subtracted moneys in 5.6425 ms 
Subtracted decimals in 31.5431 ms 
Subtracted doubles in 1.7022 ms 
Subtracted longs in 1.7008 ms 

Multiplied moneys in 20.4474 ms 
Multiplied decimals in 24.9457 ms 
Multiplied doubles in 1.6997 ms 
Multiplied longs in 1.699 ms 

Divided moneys in 15.2841 ms 
Divided decimals in 229.7391 ms 
Divided doubles in 7.2264 ms 
Divided longs in 8.6903 ms 

Equility compared moneys in 5.3652 ms 
Equility compared decimals in 29.003 ms 
Equility compared doubles in 1.727 ms 
Equility compared longs in 1.7547 ms 

Relationally compared moneys in 9.0285 ms 
Relationally compared decimals in 29.2716 ms 
Relationally compared doubles in 1.7186 ms 
Relationally compared longs in 1.7321 ms 

Conclusiones

  1. suma, resta, multiplicación, operaciones de comparación en decimal son ~ 15 veces más lento que las operaciones en long o double; la división es ~ 30 veces más lenta.
  2. El rendimiento de Decimal -como el envoltorio es mejor que el rendimiento de Decimal pero aún es significativamente peor que el rendimiento de double y long debido a la falta de soporte de CLR.
  3. La realización de cálculos en Decimal en números absolutos es bastante rápida: 40.000.000 de operaciones por segundo.

consejos

  1. A menos que tenga un caso de cálculo muy pesado, usar decimales. En números relativos, son más lentos que los largos y dobles, pero los números absolutos se ven bien.
  2. No tiene mucho sentido volver a implementar Decimal con su propia estructura debido a la falta de soporte de CLR. Puede hacer que sea más rápido que Decimal, pero nunca será tan rápido como double.
  3. Si el rendimiento de Decimal no es suficiente para su aplicación, entonces puede considerar cambiar sus cálculos a long con precisión fija. Antes de devolver el resultado al cliente, debe convertirse a Decimal.