¿Qué hacer con el rendimiento de Java BigDecimal?

Escribo aplicaciones de comercio de divisas para vivir, así que tengo que trabajar con valores monetarios (es una pena que Java todavía no tenga tipo de flotación decimal y no tenga nada que respalde los cálculos monetarios de precisión arbitraria). “¡Usa BigDecimal!” – tu podrias decir. Hago. Pero ahora tengo algún código donde el rendimiento es un problema, y ​​BigDecimal es más de 1000 veces (!) Más lento que double primitivas double .

Los cálculos son muy simples: lo que el sistema hace es calcular a = (1/b) * c muchas veces (donde a , c son valores de punto fijo). El problema, sin embargo, se encuentra con esto (1/b) . No puedo usar la aritmética de punto fijo porque no hay un punto fijo. Y el BigDecimal result = a.multiply(BigDecimal.ONE.divide(b).multiply(c) no solo es feo, sino que es lento.

¿Qué puedo usar para reemplazar BigDecimal? Necesito al menos 10 veces más de rendimiento. Encontré una excelente biblioteca de JScience que tiene aritmética de precisión arbitraria, pero es incluso más lenta que BigDecimal.

¿Alguna sugerencia?

¿Puede ser que debería comenzar reemplazando a = (1 / b) * c con a = c / b? No es 10x, pero sigue siendo algo.

Si yo fuera tú, crearía mi propia clase Money, que mantendría dólares largos y centavos largos, y haría matemática en ella.

Así que mi respuesta original fue totalmente errónea, porque mi punto de referencia estaba mal escrito. Supongo que soy yo quien debería haber sido criticado, no OP;) Este puede haber sido uno de los primeros puntos de referencia que he escrito … bueno, así es como aprendes. En lugar de eliminar la respuesta, aquí están los resultados en los que no estoy midiendo lo incorrecto. Algunas notas:

  • Precalcular las matrices para no interferir con los resultados generando
  • No BigDecimal.doubleValue() llamar a BigDecimal.doubleValue() , ya que es extremadamente lento
  • No te metas con los resultados al agregar BigDecimal s. Simplemente devuelva un valor y use una instrucción if para evitar la optimización del comstackdor. Sin embargo, asegúrese de que funcione la mayor parte del tiempo para permitir que la predicción de bifurcación elimine esa parte del código.

Pruebas:

  • BigDecimal: haz los cálculos exactamente como lo sugieres
  • BigDecNoRecip: (1 / b) * c = c / b, solo haz c / b
  • Doble: hacer las matemáticas con dobles

Aquí está el resultado:

  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 

Aquí está el código:

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

La mayoría de las operaciones dobles te dan una precisión más que suficiente. Puede representar $ 10 trillones con una precisión de ciento con doble, lo que puede ser más que suficiente para usted.

En todos los sistemas de negociación en los que he trabajado (cuatro bancos diferentes), han usado el doble con el redondeo apropiado. No veo ninguna razón para usar BigDecimal.

Suponiendo que puede trabajar con una precisión arbitraria pero conocida (digamos una millonésima de un centavo) y tiene un valor máximo conocido que necesita manejar (¿un billón de billones de dólares?) Puede escribir una clase que almacene ese valor como un número entero de milmillonésimas de un centavo Necesitarás dos largos para representarlo. Eso debería ser tal vez diez veces más lento que usar el doble; aproximadamente cien veces más rápido que BigDecimal.

La mayoría de las operaciones solo realizan la operación en cada parte y se renormalizan. La división es un poco más complicada, pero no mucho.

EDITAR: En respuesta al comentario. Tendrá que implementar una operación de cambio de bits en su clase (tan fácil como el multiplicador para el alto largo es una potencia de dos). Para hacer la división, cambie el divisor hasta que no sea más grande que el dividendo; restar el divisor desplazado del dividendo e incrementar el resultado (con el desplazamiento apropiado). Repetir.

EDITAR DE NUEVO: es posible que BigInteger haga lo que necesita aquí.

Tienda longs como la cantidad de centavos. Por ejemplo, BigDecimal money = new BigDecimal ("4.20") convierte en long money = 420 . Solo tienes que recordar modificar por 100 para obtener dólares y centavos por producción. Si necesita rastrear, digamos, décimas de centavo, se convertiría en long money = 4200 lugar.

Es posible que desee pasar a matemática de punto fijo. Solo estoy buscando algunas bibliotecas ahora mismo. en el punto fijo de sourceforge, aún no lo he analizado en profundidad. beartonics

¿Has probado con org.jscience.economics.money? ya que eso ha asegurado la precisión. El punto fijo solo será tan preciso como el número de bits asignados a cada pieza, pero es rápido.

Recuerdo haber asistido a una presentación de ventas de IBM para una implementación acelerada por hardware de BigDecimal. Entonces, si su plataforma objective es IBM System z, o System p, podría aprovechar esto sin problemas.

El siguiente enlace puede ser de alguna utilidad.

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

Actualización: el enlace ya no funciona.

¿Qué versión de JDK / JRE estás usando?

También puede probar ArciMath BigDecimal para ver si la suya lo acelera.

Editar:

Recuerdo haber leído en algún lado (creo que fue Effective Java) que la clase BigDecmal pasó de llamarse JNI a una biblioteca C a ser Java en algún momento … y se hizo más rápido a partir de ahí. Por lo tanto, es posible que cualquier biblioteca de precisión arbitraria que utilice no le proporcione la velocidad que necesita.

Personalmente, no creo que BigDecimal sea ideal para esto.

Realmente desea implementar su propia clase Money usando longs internamente para representar la unidad más pequeña (es decir, ciento, décimo). Hay algo de trabajo en eso, implementando add() y divide() , etc., pero no es realmente tan difícil.

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

Lanzar un poco más de hardware en esto podría ser más económico (considerando la probabilidad de tener un error de cálculo de la moneda).

1 / b no es exactamente representable con BigDecimal tampoco. Consulte los documentos API para averiguar cómo se redondea el resultado.

No debería ser demasiado difícil escribir su propia clase decimal fija basada en un campo largo o dos. No conozco ninguna biblioteca apropiada.

Sé que estoy publicando en un tema muy antiguo, pero este fue el primer tema encontrado por google. Considere mover sus cálculos a la base de datos de la cual probablemente esté tomando los datos para procesarlos. También estoy de acuerdo con Gareth Davis, quien escribió:

. En la mayoría de los webapps estándar de pantano, la sobrecarga de acceso a jdbc y el acceso a otros recursos de la red impiden cualquier beneficio de tener una matemática realmente rápida.

En la mayoría de los casos, las consultas incorrectas tienen un mayor impacto en el rendimiento que la biblioteca matemática.

¿Puede proporcionar más información sobre el propósito del cálculo?

Con lo que se trata es una compensación entre velocidad y precisión. ¿Cuán grande será la pérdida de precisión si cambiaste a un primitivo?

Creo que, en algunos casos, el usuario puede sentirse cómodo con menos precisión a cambio de velocidad, siempre y cuando pueda perfeccionar el cálculo preciso cuando sea necesario. Realmente depende de para qué usará este cálculo.

Quizás pueda permitirle al usuario obtener una vista previa del resultado rápidamente usando dobles, y luego solicitar el valor más preciso usando BigDecimal si así lo desea.

¿Es JNI una posibilidad? Es posible que pueda recuperar algo de velocidad y aprovechar potencialmente las bibliotecas de punto fijo nativas existentes (tal vez incluso algo de SSE * bondad)

Quizás http://gmplib.org/

¿Tal vez deberías buscar obtener aritmética decimal acelerada por hardware?

http://speleotrove.com/decimal/

Tuve un problema similar a este en un sistema de negociación de acciones en 99. Al comienzo del diseño elegimos tener cada número en el sistema representado como un largo multiplicado por 1000000, por lo tanto, 1.3423 era 1342300L. Pero el principal impulsor de esto fue la huella de memoria en lugar del rendimiento en línea recta.

Una palabra de advertencia, no volvería a hacer esto hoy a menos que estuviera realmente seguro de que el rendimiento matemático fue muy crítico. En la mayoría de los webapps estándar de pantano, la sobrecarga de acceso a jdbc y el acceso a otros recursos de la red impiden cualquier beneficio de tener una matemática realmente rápida.

Parece que la solución más simple es utilizar BigInteger en lugar de implementar la solución pesto. Si parece complicado, sería fácil escribir una clase que ajuste BigInteger para ocultar el ajuste de precisión.

fácil … redondeando sus resultados a menudo eliminará el error del tipo de datos dobles. Si está calculando el saldo, también debe considerar quién será el propietario del centavo más / menos causado por el redondeo.

El cálculo de bigdeciaml produce más / menos centavo también, considere el caso de 100/3.

Commons Math – The Apache Commons Mathematics Library

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

De acuerdo con mi propia evaluación comparativa para mi caso de uso específico, es de 10 a 20 veces más lento que el doble (mucho mejor que 1000x), básicamente para la sum / multiplicación. Después de comparar otro algoritmo que tenía una secuencia de adiciones seguida de una exponenciación, la disminución del rendimiento fue bastante peor: 200x – 400x. Entonces parece bastante rápido para + y *, pero no exp y log.

Commons Math es una biblioteca de componentes livianos y autónomos de matemáticas y estadística que abordan los problemas más comunes que no están disponibles en el lenguaje de progtwigción Java o Commons Lang.

Nota: La API protege a los constructores para forzar un patrón de fábrica al nombrar el DfpField de fábrica (en lugar del DfpFac o DfpFactory algo más intuitivo). Entonces debes usar

 new DfpField(numberOfDigits).newDfp(myNormalNumber) 

para instanciar un Dfp, entonces puede llamar a .multiply o lo que sea. Pensé en mencionar esto porque es un poco confuso.

En una JVM de 64 bits, la creación de su BigDecimal de la siguiente manera lo hace aproximadamente 5 veces más rápido:

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