web-dev-qa-db-fra.com

Que faire avec les performances Java BigDecimal?

J'écris des applications de trading de devises pour vivre, je dois donc travailler avec des valeurs monétaires (il est dommage que Java n'ait toujours pas de type décimal float et n'ait rien pour supporter des calculs monétaires de précision arbitraire). "Utilisez BigDecimal!" - tu pourrais dire. Je fais. Mais maintenant, j’ai un code où la performance est un problème et BigDecimal est plus de 1000 fois (!) Plus lent que les primitives double.

Les calculs sont très simples: le système calcule a = (1/b) * c plusieurs fois (où a, b et c sont des valeurs à point fixe). Le problème, cependant, réside avec ce (1/b). Je ne peux pas utiliser l'arithmétique à point fixe car il n'y a pas de point fixe. Et BigDecimal result = a.multiply(BigDecimal.ONE.divide(b).multiply(c) n’est pas seulement laid, mais aussi lentement.

Que puis-je utiliser pour remplacer BigDecimal? J'ai besoin d'au moins 10 fois plus de performances. J'ai trouvé sinon excellent la bibliothèque JScience qui a des arithmétiques de précision arbitraire, mais c'est encore plus lent que BigDecimal.

Aucune suggestion?

58

Peut-être devriez-vous commencer par remplacer a = (1/b) * c par a = c/b? Ce n'est pas 10x, mais quand même quelque chose.

Si j'étais vous, je créerais ma propre classe Money, qui garderait des dollars longs et des cents longs et ferait les calculs en conséquence. 

34
Vladimir Dyuzhev

Donc, ma réponse initiale était tout simplement fausse, parce que mon repère était mal écrit. Je suppose que c'est moi qui aurais dû être critiqué, pas OP;) Cela a peut-être été l'un des premiers points de repère que j'ai jamais écrit ... eh bien, c'est comme ça que vous apprenez. Plutôt que de supprimer la réponse, voici les résultats pour lesquels je ne mesure pas la mauvaise chose. Quelques notes:

  • Précalculez les tableaux pour ne pas déranger les résultats en les générant
  • Ne pas jamais appeler BigDecimal.doubleValue(), car il est extrêmement lent
  • Ne jouez pas avec les résultats en ajoutant BigDecimals. Renvoyez simplement une valeur et utilisez une instruction if pour empêcher l'optimisation du compilateur. Assurez-vous qu'il fonctionne la plupart du temps pour permettre à la prédiction de branche d'éliminer cette partie du code.

Tests:

  • BigDecimal: faites le calcul exactement comme vous l'avez suggéré
  • BigDecNoRecip: (1/b) * c = c/b, il suffit de faire c/b
  • Double: faites le calcul avec les doubles

Voici la sortie:

 0% Scenario{vm=Java, trial=0, benchmark=Double} 0.34 ns; ?=0.00 ns @ 3 trials
33% Scenario{vm=Java, trial=0, benchmark=BigDecimal} 356.03 ns; ?=11.51 ns @ 10 trials
67% Scenario{vm=Java, trial=0, benchmark=BigDecNoRecip} 301.91 ns; ?=14.86 ns @ 10 trials

    benchmark      ns linear runtime
       Double   0.335 =
   BigDecimal 356.031 ==============================
BigDecNoRecip 301.909 =========================

vm: Java
trial: 0

Voici le code:

import Java.math.BigDecimal;
import Java.math.MathContext;
import Java.util.Random;

import com.google.caliper.Runner;
import com.google.caliper.SimpleBenchmark;

public class BigDecimalTest {
  public static class Benchmark1 extends SimpleBenchmark {
    private static int ARRAY_SIZE = 131072;

    private Random r;

    private BigDecimal[][] bigValues = new BigDecimal[3][];
    private double[][] doubleValues = new double[3][];

    @Override
    protected void setUp() throws Exception {
      super.setUp();
      r = new Random();

      for(int i = 0; i < 3; i++) {
        bigValues[i] = new BigDecimal[ARRAY_SIZE];
        doubleValues[i] = new double[ARRAY_SIZE];

        for(int j = 0; j < ARRAY_SIZE; j++) {
          doubleValues[i][j] = r.nextDouble() * 1000000;
          bigValues[i][j] = BigDecimal.valueOf(doubleValues[i][j]); 
        }
      }
    }

    public double timeDouble(int reps) {
      double returnValue = 0;
      for (int i = 0; i < reps; i++) {
        double a = doubleValues[0][reps & 131071];
        double b = doubleValues[1][reps & 131071];
        double c = doubleValues[2][reps & 131071];
        double division = a * (1/b) * c; 
        if((i & 255) == 0) returnValue = division;
      }
      return returnValue;
    }

    public BigDecimal timeBigDecimal(int reps) {
      BigDecimal returnValue = BigDecimal.ZERO;
      for (int i = 0; i < reps; i++) {
        BigDecimal a = bigValues[0][reps & 131071];
        BigDecimal b = bigValues[1][reps & 131071];
        BigDecimal c = bigValues[2][reps & 131071];
        BigDecimal division = a.multiply(BigDecimal.ONE.divide(b, MathContext.DECIMAL64).multiply(c));
        if((i & 255) == 0) returnValue = division;
      }
      return returnValue;
    }

    public BigDecimal timeBigDecNoRecip(int reps) {
      BigDecimal returnValue = BigDecimal.ZERO;
      for (int i = 0; i < reps; i++) {
        BigDecimal a = bigValues[0][reps & 131071];
        BigDecimal b = bigValues[1][reps & 131071];
        BigDecimal c = bigValues[2][reps & 131071];
        BigDecimal division = a.multiply(c.divide(b, MathContext.DECIMAL64));
        if((i & 255) == 0) returnValue = division;
      }
      return returnValue;
    }
  }

  public static void main(String... args) {
    Runner.main(Benchmark1.class, new String[0]);
  }
}
13
durron597

La plupart des doubles opérations vous donnent plus de précision. Vous pouvez représenter 10 000 milliards de dollars avec une précision d'un cent avec un double, ce qui peut être plus que suffisant pour vous.

Dans tous les systèmes de négociation sur lesquels j'ai travaillé (quatre banques différentes), ils ont utilisé le double avec un arrondi approprié. Je ne vois aucune raison d'utiliser BigDecimal.

13
Peter Lawrey

En supposant que vous puissiez travailler avec une précision arbitraire mais connue (disons un milliardième de cent) et que vous ayez une valeur maximale connue que vous devez gérer (un billion de milliards de dollars?), Vous pouvez écrire une classe qui stocke cette valeur sous forme de nombre entier de un centime Vous aurez besoin de deux longs pour le représenter. Cela devrait être peut-être dix fois plus lent que d'utiliser double; environ cent fois plus vite que BigDecimal.

La plupart des opérations consistent simplement à effectuer l'opération sur chaque partie et à la renormaliser. La division est un peu plus compliquée, mais pas beaucoup.

EDIT: En réponse au commentaire. Vous devrez implémenter une opération de décalage de bits sur votre classe (aussi facile que le multiplicateur pour le haut long est une puissance de deux). Pour diviser le diviseur en diviseur jusqu'à ce qu'il ne soit pas tout à fait supérieur au dividende; soustrayez le diviseur décalé du dividende et incrémentez le résultat (avec le décalage approprié). Répéter.

EDIT AGAIN: Vous pouvez trouver que BigInteger fait ce dont vous avez besoin ici.

9
DJClayworth

Stocke en tant que nombre de centimes. Par exemple, BigDecimal money = new BigDecimal ("4.20") devient long money = 420. N'oubliez pas de modifier 100 pour obtenir des dollars et des cents pour la sortie. Si vous avez besoin de suivre, disons, des dixièmes de cent, il deviendrait plutôt long money = 4200.

5
Pesto

Vous voudrez peut-être passer au calcul mathématique à point fixe. Je cherche juste quelques bibliothèques en ce moment ... sur Sourceforge point fixe Je n'ai pas encore regardé cela en profondeur. Beartonics

Avez-vous testé avec org.jscience.economics.money? puisque cela a assuré l'exactitude. Le point fixe sera seulement aussi précis que le nombre de bits assignés à chaque morceau, mais est rapide.

5
sfossen

Je me souviens d'avoir assisté à une présentation commerciale d'IBM pour une implémentation accélérée matérielle de BigDecimal. Par conséquent, si votre plate-forme cible est IBM System z ou System p, vous pouvez l'exploiter de manière transparente.

Le lien suivant pourrait être utile.

http://www-03.ibm.com/servers/enable/site/education/wp/181ee/181ee.pdf

Mise à jour: le lien ne fonctionne plus.

3
toolkit

Quelle version du JDK/JRE utilisez-vous?

Vous pouvez aussi essayer ArciMath BigDecimal pour voir si le leur accélère pour vous.

Modifier:

Je me souviens avoir lu quelque part (j’ai pensé que c’était Effective Java) que la classe BigDecmal avait été modifiée: elle n’était plus appelée JNI mais devenait une bibliothèque C et java en Java ... Il se peut donc que toute bibliothèque de précision arbitraire que vous utilisez ne vous procure pas la vitesse dont vous avez besoin.

2
TofuBeer
Only 10x performance increase desired for something that is 1000x slower than primitive?!.

Lancer un peu plus de matériel sur ceci pourrait être moins cher (compte tenu de la probabilité d'avoir une erreur de calcul de devise). 

2
Ryan Fernandes

Personnellement, je ne pense pas que BigDecimal soit idéal pour cela.

Vous voulez vraiment implémenter votre propre classe Money en utilisant des positions longues en interne pour représenter la plus petite unité (c’est-à-dire cent, 10 cent) Il y a du travail à faire, mettre en œuvre add() et divide() etc., mais ce n'est pas si difficile.

2
SCdF

facile ... arrondir vos résultats éliminera souvent l'erreur du double type de données . si vous faites le calcul de la balance, vous devez également considérer qui sera propriétaire du centime plus/moins causé par l'arrondissement. 

calcul bigdeciaml produit plus/moins penny aussi, considérons 100/3 cas.

1
Chris

1/b n'est pas non plus exactement représentable avec BigDecimal. Consultez la documentation de l'API pour savoir comment le résultat est arrondi.

Il ne devrait pas être trop difficile d'écrire votre propre classe décimale fixe basée sur un champ long ou deux. Je ne connais pas de bibliothèques prêtes à l'emploi.

1

Je sais que je poste sous un sujet très ancien, mais c’était le premier sujet trouvé par Google . Pensez à transférer vos calculs dans la base de données à partir de laquelle vous prenez probablement les données pour traitement. Aussi je suis d'accord avec Gareth Davis qui a écrit:

. Dans la plupart des applications Web standard de bog, la surcharge d'accès jdbc et d'accès à un autre réseau ressources submerge tout avantage d'avoir des calculs très rapides.

Dans la plupart des cas, les requêtes incorrectes ont un impact plus important sur les performances que la bibliothèque mathématique.

1
Marek Kowalczyk

Nous avions un problème similaire à celui rencontré dans un système de négociation d’actions en 99. Au tout début de la conception, nous avons choisi de faire en sorte que tous les nombres du système soient représentés par un long nombre multiplié par 1000000, de sorte que 1,3423 était 1342300L. Mais le principal facteur à cet égard était l’empreinte mémoire au lieu des performances en ligne droite.

Un mot sur la prudence, je ne le ferais plus aujourd'hui si j'étais vraiment sûr que les performances en mathématiques étaient super critiques. Dans la plupart des applications Web standard standardisées, l’accès jdbc et l’accès à d’autres ressources réseau nuisent à l’avantage d’avoir un calcul très rapide.

0
Gareth Davis

Il semble que la solution la plus simple consiste à utiliser BigInteger au lieu de mettre longtemps à implémenter la solution de pesto. Si cela semble compliqué, il serait facile d'écrire une classe qui englobe BigInteger pour masquer le réglage de précision.

0
ozone

JNI est une possibilité? Vous pourrez peut-être retrouver un peu de vitesse et exploiter potentiellement les bibliothèques de points fixes natifs existantes (peut-être même certains avantages de SSE * également)

Peut-être http://gmplib.org/

0
basszero

Pouvez-vous donner plus de détails sur l'objectif du calcul? 

Votre affaire est un compromis entre vitesse et précision. Quelle sera l'ampleur de la perte de précision si vous passez à une primitive?

Je pense que dans certains cas, l'utilisateur peut être à l'aise avec moins de précision en échange de vitesse, à condition qu'il puisse se concentrer sur le calcul précis en cas de besoin. Cela dépend vraiment de la raison pour laquelle vous utiliserez ce calcul.

Peut-être pouvez-vous permettre à l'utilisateur de prévisualiser rapidement le résultat à l'aide de doubles, puis de demander la valeur plus précise à l'aide de BigDecimal s'il le souhaite?

0
Justin Standard

Commons Math - La bibliothèque de mathématiques Apache Commons

http://mvnrepository.com/artifact/org.Apache.commons/commons-math3/3.2

Selon mes propres analyses comparatives pour mon cas d'utilisation spécifique, il est de 10 à 20 fois plus lent que le double (bien mieux que 1000 x), essentiellement pour l'addition/la multiplication. Après avoir comparé un autre algorithme comportant une séquence d’additions suivie d’une exponentiation, la baisse de performance était bien pire: 200x - 400x. Cela semble donc assez rapide pour + et *, mais pas pour exp et log.

Commons Math est une bibliothèque de composants statistiques et statistiques légers et autonomes, abordant les problèmes les plus courants, non disponible dans le langage de programmation Java ou Commons Lang.

Remarque: L'API protège les constructeurs pour forcer un modèle d'usine tout en nommant la fabrique DfpField (plutôt que DfpFac ou DfpFactory un peu plus intuitif). Donc vous devez utiliser 

new DfpField(numberOfDigits).newDfp(myNormalNumber)

pour instancier un Dfp, vous pouvez appeler .multiply ou quoi que ce soit à ce sujet. Je pensais en parler parce que c'est un peu déroutant.

0
samthebest

Sur une machine virtuelle Java 64 bits, créer votre BigDecimal comme ci-dessous accélère environ 5 fois:

BigDecimal bd = new BigDecimal(Double.toString(d), MathContext.DECIMAL64);
0
tsquared

Peut-être devriez-vous chercher à obtenir des arithmétiques décimales à accélération matérielle?

http://speleotrove.com/decimal/

0
John Nilsson