Есть ли способ сделать BigDecimal быстрее, чем у меня здесь?

#java #kotlin #bigdecimal

#java #kotlin #bigdecimal

Вопрос:

Я работаю над некоторым программным обеспечением для финансового анализа, которому потребуется обрабатывать большие объемы данных. Я бы хотел использовать BigDecimal для точности (некоторая информация о ценах содержит четыре или пять цифр справа от десятичной дроби), но меня беспокоила скорость.

Я написал следующее тестовое приложение, и оказалось, что BigDecimal может быть в 90-100 раз медленнее, чем Double. Я знал, что будет дельта, но это больше, чем я ожидал. Вот типичный результат после многих испытаний.

 BigDecimal took 17944 ms
Double took 181 ms
  

Я что-то упускаю?

Вот код. Я попытался сделать его репрезентативным для реального мира. Я создал константу, где я мог (pi), но также выполнил некоторую встроенную математику чисел, которые будут варьироваться от строки данных к строке данных, например, pi * BigDecimal(i) BigDecimal(1) . Я хочу сказать, что избегание конструкторов не может быть единственным ответом.

К счастью, похоже, что Double в любом случае обладает достаточной точностью, поскольку числа обычно будут в формате 00000.00000. Какие-нибудь скрытые ошибки, о которых я должен знать? Люди используют Double для программного обеспечения для финансового анализа?

 import java.math.BigDecimal

object Stopwatch {
    inline fun elapse(f: () -> Unit):Long {
        val start = System.currentTimeMillis()
        f()
        return System.currentTimeMillis() - start
    }
}


fun tryBigDecimal() {
    val arr: MutableList<BigDecimal> = arrayListOf()

    for (i in 1..10000000) {
        arr.add(BigDecimal(i))
    }

    val pi = BigDecimal(3.14159)

    for (i in 0..arr.size - 1) {
        arr[i] = arr[i] * pi / (pi * BigDecimal(i)   BigDecimal(1))
    }

    //arr.forEachIndexed { i, bigDecimal ->  println("$i, ${bigDecimal.toString()}")}
}

fun tryDouble() {
    val arr: MutableList<Double> = arrayListOf()

    for (i in 1..10000000) {
        arr.add(i.toDouble())
    }

    val pi = 3.14159

    for (i in 0..arr.size - 1) {
        arr[i] = arr[i] * pi / (pi * i   1)
    }

    //arr.forEachIndexed { i, bigDecimal ->  println("$i, ${bigDecimal.toString()}")}

}

fun main(args: Array<String>) {
    val bigdecimalTime = Stopwatch.elapse(::tryBigDecimal)
    println("BigDecimal took $bigdecimalTime ms")

    val doubleTime = Stopwatch.elapse(::tryDouble)
    println("Double took $doubleTime ms")
}
  

Комментарии:

1. Не используйте double, когда важна точность, например, с финансами. Это может иметь необходимую точность, но не точность. Используйте int /long, если можете, и BigDecimal, если не можете, но никогда не используйте float / double .

2. Поскольку вы нашли время, чтобы предоставить конструктивную обратную связь, которую я ценю, я хотел бы потратить время, чтобы предоставить некоторые из них и вам. Ваш ответ мог быть написан как «Я вижу, где у вас может быть проблема. Возможно, оптимизатор устранил большую часть кода в разделе Double, что действительно сделало бы тест недействительным. Возможно, вы захотите проверить JMH, который может помочь именно с такой проблемой. » Агрессия и высокомерие вашего ответа, возможно, не были преднамеренными, но они были сильными. Просто говорю…

3. Черт возьми, чего вам не хватает, так это того, что double аппаратно поддерживается в большинстве систем, в то время BigDecimal как это чисто программный тип. Не уверен, какую роль играет оптимизатор, но вы должны ожидать значительной разницы в скорости.

4. @vocalionecho спасибо за ответ. Я прошу прощения. Тем не менее, JMH необходим для оценки любых альтернатив по этой проблеме.

5. Я искренне признателен за помощь @voddan. Вы, очевидно, знаете, о чем говорите. Я обязательно загляну в JMH.

Ответ №1:

Да, BigDecimal подходит для денег. Или любая другая ситуация, когда вам нужна точность, а не скорость.

С плавающей запятой

Все float типы , Float , double , и Double используют технологию с плавающей запятой.

Цель с плавающей запятой — обменять точность на скорость выполнения. Таким образом, вы часто видите посторонние неправильные цифры в конце десятичной дроби. Это приемлемо для игр, 3D-визуализации и многих научных приложений. Компьютеры обычно имеют специализированное оборудование для ускорения вычислений с плавающей запятой. Это возможно, потому что IEEE имеет конкретно стандартизированное поведение с плавающей запятой.

Плавающая точка неприемлема для финансовых транзакций. Плавающая точка также не приемлема в любой другой ситуации, которая ожидает правильных дробей.

BigDecimal

Две цели BigDecimal :

  • Обрабатывать произвольно большое / малое число.
  • Не используйте технологию с плавающей запятой.

Итак, что нужно вашему приложению? Медленно, но точно? Или быстро, но немного неточно? Это ваш выбор. Компьютеры — это не волшебство, компьютеры не бесконечно быстрые и не бесконечно точные. Программирование похоже на разработку в том смысле, что все сводится к выбору компромиссов в соответствии с потребностями вашего конкретного приложения.

BigDecimal это одна из самых больших спящих функций в Java. Блестящая работа IBM и других. Я не знаю, есть ли у какой-либо другой платформы разработки такое превосходное средство для точной обработки десятичных чисел. Посмотрите некоторые презентации JavaOne, сделанные несколько лет назад, если вы хотите оценить технические проблемы.

Не инициализируйте объект BigDecimal, передавая значение float или double:

 new BigDecimal( 1234.4321 )  // BAD - Do not do this.
  

Этот аргумент создает float значение, которое вводит неточности технологии с плавающей запятой. Используйте другие конструкторы.

 new BigDecimal( "1234.4321" )  // Good
  

Ответ №2:

Вы можете попробовать Moneta, эталонную реализацию JSR 354 (JavaMoney RI). У него есть FastMoney реализация:

FastMoney представляет числовое представление, оптимизированное для скорости. Он представляет денежную сумму только как целое число типа long , при этом используя числовую шкалу 100000 (10 ^ 5).

например

 operator fun MonetaryAmount.times(multiplicand: Double): MonetaryAmount {
    return multiply(multiplicand)
}

operator fun MonetaryAmount.div(divisor: Double): MonetaryAmount {
    return divide(divisor)
}

fun tryFastMoney() {
    val currency = Monetary.getCurrency("USD")
    val arr: MutableList<MonetaryAmount> = arrayListOf()

    for (i in 1..10000000) {
        arr.add(FastMoney.of(i, currency))
    }

    val pi = 3.14159

    for (i in 0..arr.size - 1) {
        arr[i] = arr[i] * pi / (pi * i   1)
    }
}

fun main(args: Array<String>) {
    val fastMoneyTime = Stopwatch.elapse(::tryFastMoney)
    println("FastMoney took $fastMoneyTime ms")
    val doubleTime = Stopwatch.elapse(::tryDouble)
    println("Double took $doubleTime ms")
}
  
 FastMoney took 7040 ms
Double took 4319 ms
  

Ответ №3:

Наиболее распространенным решением для финансов является использование Int или несколько Int :

 val pi = 314159  // the point is implicit. To get the real value multiply `pi * 0.00001` 
  

Таким образом, вы явно контролируете все, что касается чисел (например, остатки после деления).

Вы можете использовать Long , но он не является атомарным, и, следовательно, он небезопасен одновременно. Это означает, что вам нужно синхронизировать любой общий Long ресурс, который у вас есть.

Эмпирическое правило состоит в том, чтобы никогда не использовать арифметику с плавающей запятой (например Double , или Float ) для финансов, потому что, ну, его точка плавает, что абсолютно ничего не гарантирует, когда числа большие.