web-dev-qa-db-fra.com

Performances de type données décimales C #

J'écris une application financière en C # où la performance (c'est-à-dire la vitesse) est essentielle. Parce que c'est une application financière, je dois utiliser le type de données Decimal de manière intensive. 

J'ai optimisé le code autant que possible avec l'aide d'un profileur. Avant d'utiliser Decimal, tout était fait avec le type de données Double et la vitesse était plusieurs fois plus rapide. Toutefois, Double n’est pas une option en raison de sa nature binaire, ce qui entraîne de nombreuses erreurs de précision au cours de plusieurs opérations.

Existe-t-il une bibliothèque décimale que je peux interfacer avec C # qui pourrait améliorer considérablement les performances par rapport au type de données Decimal natif dans .NET?

Sur la base des réponses que j'ai déjà obtenues, j'ai remarqué que je n'étais pas assez clair. Voici donc quelques détails supplémentaires:

  • L’application doit être aussi rapide que possible (c’est-à-dire aussi rapide que lorsque vous utilisiez Double au lieu de Décimal, ce serait un rêve). Double était environ 15 fois plus rapide que Decimal, car les opérations sont basées sur le matériel.
  • Le matériel est déjà de premier ordre (je l’utilise sur un Dual Xenon Quad-Core) et l’application utilise des threads, de sorte que l’utilisation du processeur est toujours à 100% sur la machine. En outre, l'application fonctionne en mode 64 bits, ce qui lui confère un avantage de performances mensurable par rapport à 32 bits.
  • J'ai optimisé au-delà du seuil de cohérence (plus d'un mois et demi d'optimisation; croyez-le ou non, il faut maintenant environ 1/5 000 de ce qu'il a fallu pour effectuer les mêmes calculs que ceux utilisés à l'origine comme référence); cette optimisation impliquait tout: traitement des chaînes, entrées/sorties, accès à la base de données et index, mémoire, boucles, modification du processus de création, et même utilisation de "commutateur" sur "si" partout. Le profileur montre maintenant clairement que le dernier coupable en termes de performances se trouve sur les opérateurs de type de données Decimal. Rien d'autre ne prend beaucoup de temps.
  • Vous devez me croire ici: je suis allé aussi loin que possible dans le domaine de C # .NET pour optimiser l'application, et je suis vraiment étonné de ses performances actuelles. Je cherche maintenant une bonne idée pour améliorer les performances de Decimal à un niveau proche de Double. Je sais que ce n'est qu'un rêve, mais je voulais juste vérifier que je pensais à tout ce qui était possible. :)

Merci!

53
tempw

vous pouvez utiliser le type de données long. Bien sûr, vous ne pourrez pas y stocker de fractions, mais si vous codez votre application pour qu'elle stocke des sous au lieu de deux kilos, tout ira bien. La précision est de 100% pour les types de données longs et, à moins que vous ne travailliez avec de grands nombres (utilisez un type long de 64 bits), tout ira bien.

Si vous ne pouvez pas imposer de stocker des sous, insérez un entier dans une classe et utilisez-le.

40
gbjbaanb

Vous dites que cela doit être rapide, mais avez-vous des exigences concrètes en matière de vitesse? Sinon, vous pouvez optimiser le passé.

Comme un ami assis à côté de moi vient de le suggérer, pouvez-vous mettre à niveau votre matériel? C'est probablement moins cher que de réécrire du code.

L'option la plus évidente consiste à utiliser des nombres entiers au lieu de décimales - où une "unité" est quelque chose comme "un millième de cent" (ou ce que vous voulez - vous voyez l'idée). Que cela soit réalisable ou non dépendra des opérations que vous effectuez sur les valeurs décimales pour commencer. Vous devrez être très prudent lorsque vous vous en occuperez - il est facile de faire des erreurs (du moins si vous êtes comme moi).

Le profileur a-t-il indiqué dans votre application des zones sensibles que vous pouviez optimiser individuellement? Par exemple, si vous devez effectuer de nombreux calculs dans une petite zone de code, vous pouvez convertir du format décimal au format entier, effectuer les calculs puis reconvertir. Cela pourrait garder leAPIen termes de nombres décimaux pour l’essentiel du code, ce qui pourrait en faciliter la maintenance. Cependant, si vous n'avez pas de points chauds prononcés, cela risque de ne pas être réalisable.

+1 pour le profilage et nous dire que la vitesse est une exigence définie, au fait :)

22
Jon Skeet

Le problème est fondamentalement que les doubles/float sont supportés dans le matériel, alors que Decimal et autres ne le sont pas. C'est à dire. vous devez choisir entre vitesse + précision limitée et plus grande précision + performances plus faibles. 

8
Brian Rasmussen

La question est bien discutée, mais depuis que je creuse ce problème depuis un moment, je voudrais partager certains de mes résultats.

Définition du problème: Les décimales sont connues pour être beaucoup plus lentes que les doubles, mais les applications financières ne peuvent tolérer aucun artefact pouvant survenir lorsque les calculs sont effectués sur des doubles.

Recherche

Mon but était de mesurer différentes approches de stockage de nombres à virgule flottante et de tirer une conclusion à utiliser pour notre application.

Si nous pouvions utiliser Int64 pour stocker des nombres à virgule flottante avec une précision fixe. Un multiplicateur de 10 ^ 6 nous donnait les deux: assez de chiffres pour stocker des fractions et une large plage pour stocker de grandes quantités. Bien sûr, vous devez faire attention à cette approche (les opérations de multiplication et de division peuvent devenir délicates), mais nous étions prêts et voulions également mesurer cette approche. Une chose à ne pas oublier, à l'exception des erreurs de calcul et des débordements possibles, est que vous ne pouvez généralement pas exposer ces nombres longs à des API publiques. Tous les calculs internes peuvent donc être effectués avec des longs, mais avant d'envoyer les numéros à l'utilisateur, ils doivent être convertis en quelque chose de plus convivial.

J'ai implémenté une classe de prototype simple qui englobe une valeur longue dans une structure de type décimale (appelée Money) et l'a ajoutée aux mesures.

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

Expérience

J'ai mesuré les opérations suivantes: addition, soustraction, multiplication, division, comparaison d'égalité et comparaison relative (plus/moins). Je mesurais des opérations sur les types suivants: double, long, decimal et Money. Chaque opération a été effectuée 1 000 000 fois. Tous les numéros ont été pré-alloués dans des tableaux. Par conséquent, l'appel de code personnalisé dans les constructeurs de decimal et Money ne devrait pas affecter les résultats.

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

Conclusions

  1. Les opérations d’addition, de soustraction, de multiplication et de comparaison sur decimal sont environ 15 fois plus lentes que les opérations sur long ou double; la division est environ 30 fois plus lente.
  2. Les performances de type Decimal- like sont meilleures que celles de Decimal, mais néanmoins nettement inférieures à celles de double et long en raison du manque de soutien de la part du CLR.
  3. Effectuer des calculs sur Decimal en nombres absolus est assez rapide: 40 000 000 opérations par seconde.

Conseil

  1. Sauf si le cas de calcul est très lourd, utilisez des décimales. En nombres relatifs, ils sont plus lents que les longs et les doubles, mais les nombres absolus semblent bons.
  2. Il est inutile de ré-implémenter Decimal avec votre propre structure en raison de l’absence de soutien de la part du CLR. Vous pourriez le rendre plus rapide que Decimal mais ce ne sera jamais aussi rapide que double.
  3. Si les performances de Decimal ne suffisent pas pour votre application, vous pouvez envisager de passer vos calculs à long avec une précision fixe. Avant de renvoyer le résultat au client, il doit être converti en Decimal.
5
user1921819

Je ne pense pas que les instructions SSE2 pourraient facilement fonctionner avec les valeurs décimales .NET. Le type de données .NET décimal est virgule flottante décimale 128 bits type http://en.wikipedia.org/wiki/Decimal128_floating-point_format , les instructions SSE2 fonctionnent avec types d'entiers 128 bits .

3
Sergey Shandar

Qu'en est-il de MMX/SSE/SSE2?

je pense que cela aidera ... donc ... decimal est un type de données de 128 bits et SSE2 est aussi de 128 bits ... et il peut ajouter, sous, div, mul décimal en 1 tick CPU. ..

vous pouvez écrire DLL pour SSE2 à l'aide de VC++, puis utiliser ce DLL dans votre application

par exemple // vous pouvez faire quelque chose comme ceci

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
Runknown

Vieille question, toujours très valable cependant. 

Voici quelques chiffres à l’appui de l’idée d’utiliser Long.

Temps nécessaire pour effectuer 100'000'000 ajouts

Long     231 mS
Double   286 mS
Decimal 2010 mS

en un mot, le point décimal est environ 10 fois plus lent que le long ou le double.

Code:

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
2
smirkingman

Je ne peux pas encore commenter ni voter, car je viens juste de commencer avec un débordement de pile. Mon commentaire sur alexsmart (23 décembre 2008 12:31) est que l'expression Round (n/precision, precision), où n est int et precisions est longue ne fera pas ce qu'il pense: 

1) n/precision retournera une division entière, c’est-à-dire qu’elle sera déjà arrondie, mais vous ne pourrez utiliser aucune décimale. Le comportement d'arrondi est également différent de Math.Round (...).

2) Le code " return Math.Round (n/precision, precision) .ToString () " ne compile pas en raison d'une ambiguïté entre Math.Round (double, int) et Math.Round (decimal, int) . Vous devrez choisir un nombre décimal (et non doubler puisqu'il s'agit d'une application financière). Vous pouvez donc également utiliser le format décimal en premier lieu.

3) n/precision, où la précision est 4 et ne sera pas tronqué à quatre décimales mais divisé par 4. Par exemple, Math.Round ((décimal) (1234567/4), 4) renvoie 308641. (1234567/4 = 308641.75), alors que vous souhaitiez probablement obtenir 1235000 (arrondi à une précision de 4 chiffres à partir du 567 suivant). Notez que Math.Round permet d’arrondir à un point fixe, pas à une précision fixe.

Mise à jour: Je peux ajouter des commentaires maintenant, mais il n'y a pas assez d'espace pour placer celui-ci dans la zone de commentaire.

1
ILoveFortran

magasin "pennies" en utilisant double. à part l'analyse des entrées et l'impression des sorties, vous avez la même vitesse que vous avez mesurée. vous surmontez la limite de 64 bits. vous avez une division ne tronquant pas. remarque: à vous de choisir comment utiliser le double résultat après les divisions. cela me semble l’approche la plus simple à vos besoins.

0
Massimo