Что делать с производительностью Java BigDecimal?

Я пишу приложения для торговли валютой для жизни, поэтому мне приходится работать с денежными значениями (обидно, что у Java до сих пор нет десятичного типа с плавающей запятой и нет ничего, чтобы поддерживать денежные вычисления произвольной точности). "Используйте BigDecimal!" - Вы могли бы сказать. Я делаю. Но теперь у меня есть некоторый код, где производительность является проблемой, и BigDecimal более чем в 1000 раз (!) Медленнее, чем double примитивы.

Расчеты очень просты: система вычисляет a = (1/b) * c много много раз (где a, b а также c являются значениями с фиксированной запятой). Проблема, однако, заключается в этом (1/b), Я не могу использовать арифметику с фиксированной точкой, потому что нет фиксированной точки. А также BigDecimal result = a.multiply(BigDecimal.ONE.divide(b).multiply(c) не только некрасиво, но и вяло медленно.

Что я могу использовать, чтобы заменить BigDecimal? Мне нужно как минимум 10-кратное увеличение производительности. В остальном я нашел отличную библиотеку JScience, которая имеет арифметику произвольной точности, но она даже медленнее, чем BigDecimal.

Какие-либо предложения?

21 ответ

Может быть, вам следует начать с замены a = (1/b) * c на a = c/b? Это не 10х, но все же что-то.

Если бы я был тобой, я бы создал свой собственный класс Money, который бы держал длинные доллары и длинные центы и занимался в нем математикой.

Большинство двойных операций дают вам более чем достаточную точность. Вы можете представлять 10 триллионов долларов с точностью до цента с двойной, что может быть более чем достаточно для вас.

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

Так что мой первоначальный ответ был просто неверным, потому что мой тест был написан плохо. Я думаю, что я тот, кого следовало бы подвергнуть критике, а не ОП;) Возможно, это был один из первых критериев, которые я когда-либо писал... ну, вот как вы учитесь. Вместо того, чтобы удалить ответ, вот результаты, где я не измеряю неправильную вещь. Некоторые заметки:

  • Пересчитайте массивы, чтобы я не связывался с результатами, генерируя их
  • Никогда не звони BigDecimal.doubleValue() очень медленно
  • Не связывайтесь с результатами, добавив BigDecimal s. Просто верните одно значение и используйте оператор if для предотвращения оптимизации компилятора. Удостоверьтесь, что он работает большую часть времени, чтобы позволить предсказание ветвлений устранить эту часть кода.

тесты:

  • BigDecimal: делайте математику в точности так, как вы предлагали
  • BigDecNoRecip: (1/b) * c = c/b, просто сделайте c / b
  • Двойной: сделать математику с двойниками

Вот вывод:

 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

Вот код:

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

Предполагая, что вы можете работать с некоторой произвольной, но известной точностью (скажем, миллиардной долей) и иметь известное максимальное значение, которое вам нужно обработать (триллион триллионов долларов?), Вы можете написать класс, который будет хранить это значение как целое число миллиардных долей цент Вам понадобится два длинных, чтобы представить это. Это должно быть в десять раз медленнее, чем при использовании double; примерно в сто раз быстрее, чем BigDecimal.

Большинство операций просто выполняют операции над каждой частью и перенормируют. Деление немного сложнее, но не намного.

РЕДАКТИРОВАТЬ: в ответ на комментарий. Вам нужно будет реализовать операцию битового сдвига в вашем классе (легко, если множитель для длинных длинных - это степень двойки). Для деления сдвиньте делитель, пока он не станет больше, чем дивиденд; вычтите смещенный делитель из дивиденда и увеличьте результат (с соответствующим смещением). Повторение.

ИЗМЕНИТЬ СНОВА: Вы можете найти, что BigInteger делает то, что вам нужно здесь.

Вы можете перейти к математике с фиксированной точкой. Просто ищу несколько библиотек прямо сейчас. на sourceforge с фиксированной точкой я еще не рассматривал это подробно. beartonics

Вы тестировали с org.jscience.economics.money? так как это обеспечило точность. Фиксированная точка будет точно такой же, как количество битов, назначенных каждому фрагменту, но быстро.

Храните длинные как количество центов. Например, BigDecimal money = new BigDecimal ("4.20") становится long money = 420, Вам просто нужно помнить мод на 100, чтобы получить доллары и центы за вывод. Если вам нужно отслеживать, скажем, десятые доли процента, это стало бы long money = 4200 вместо.

Я помню, как присутствовал на торговой презентации IBM для аппаратно ускоренной реализации BigDecimal. Поэтому, если вашей целевой платформой является IBM System z или System p, вы можете использовать это без проблем.

Следующая ссылка может быть полезной.

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

Обновление: ссылка больше не работает.

Лично я не думаю, что BigDecimal идеально подходит для этого.

Вы действительно хотите реализовать свой собственный класс Money, используя longs для представления наименьшей единицы (т. Е. Cent, 10th cent). В этом есть работа по внедрению add() а также divide() и т.д., но это не так уж сложно.

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

Потратить немного больше оборудования на это может быть дешевле (учитывая вероятность ошибки при расчете валюты).

Какую версию JDK/JRE вы используете?

Также вы можете попробовать ArciMath BigDecimal, чтобы увидеть, ускоряет ли это его для вас.

Редактировать:

Я помню, как где-то читал (я думаю, что это была Effective Java), что класс BigDecmal был изменен с того, что в какой-то момент он был вызван из JNI в библиотеку C и во всю Java... и это стало быстрее. Таким образом, может случиться так, что любая библиотека произвольной точности, которую вы используете, не даст вам необходимой скорости.

На 64-битной JVM создание вашего BigDecimal, как показано ниже, делает его примерно в 5 раз быстрее:

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

Commons Math - Математическая библиотека Apache Commons

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

Согласно моему собственному сравнительному тесту для моего конкретного случая использования, он в 10 - 20 раз медленнее, чем в два раза (намного лучше, чем в 1000 раз) - в основном для сложения / умножения. После бенчмаркинга другого алгоритма, который имел последовательность дополнений и возведения в степень, снижение производительности было немного хуже: 200x - 400x. Так что это выглядит довольно быстро для + и *, но не для exp и log.

Commons Math - это библиотека легких, автономных математических и статистических компонентов, предназначенных для решения наиболее распространенных проблем, недоступных в языке программирования Java или Commons Lang.

Примечание. API защищает конструкторы для принудительного использования фабричного шаблона, присваивая имя фабрике DfpField (а не несколько более интуитивно понятному DfpFac или DfpFactory). Так что вы должны использовать

new DfpField(numberOfDigits).newDfp(myNormalNumber)

чтобы создать экземпляр Dfp, то вы можете позвонить .multiply или что-то на этом. Я думал, что упомяну это, потому что это немного сбивает с толку.

Я знаю, что я пишу в очень старой теме, но это была первая тема, найденная Google. Подумайте о переносе ваших расчетов в базу данных, из которой вы, вероятно, берете данные для обработки. Также я согласен с Гаретом Дэвисом, который написал:

, В большинстве болотных стандартных веб-приложений накладные расходы на доступ к jdbc и доступ к другим сетевым ресурсам сводят на нет все преимущества очень быстрой математики.

В большинстве случаев неправильные запросы оказывают большее влияние на производительность, чем математическая библиотека.

1/b также не совсем представимо с BigDecimal. Посмотрите документы API, чтобы понять, как округляется результат.

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

Возможна ли JNI? Возможно, вам удастся восстановить некоторую скорость и потенциально использовать существующие нативные библиотеки с фиксированной точкой (может быть, даже некоторые преимущества SSE*)

Возможно http://gmplib.org/

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

каждый раз, когда я прокручиваю или изменяю размер окна, ему нужно будет просто перебирать кешированные значения, чтобы изменить размер шкалы Y, но даже это займет несколько секунд для обновления. это сделало приложение непригодным для использования. Каждый раз, когда я настраивал параметры для различных индикаторов, пересчет снова занимал несколько минут.

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

приложение теперь вычисляет MACD (+ сигнал MACD, гистограмма MACD) мгновенно, без задержек. Удивительно, насколько дорого обходилось создание объекта BigDecimal. подумайте о чем-то вроде a.add(b.multiply(c)).scale(3) и о том, сколько объектов создает один оператор.

       import java.util.HashMap;

public class FastDoubleMap<K>
{
    private static final Pool<Wrapper> POOL = new Pool<FastDoubleMap.Wrapper>()
    {
        protected Wrapper newInstance()
        {
            return new Wrapper();
        }
    };
    
    private final HashMap<K, Wrapper> mMap;
    
    public FastDoubleMap()
    {
        mMap = new HashMap<>();
    }

    public void put( K pKey, double pValue )
    {
        Wrapper lWrapper = POOL.checkOut();
        lWrapper.mValue = pValue;
        mMap.put( pKey, lWrapper );
    }
    
    public double get( K pKey )
    {
        Wrapper lWrapper  = mMap.get( pKey );
        if( lWrapper == null )
        {
            return Double.NaN;
        }
        else
        {
            double lDouble = lWrapper.mValue ;
            lWrapper.release();
            return lDouble;
        }
    }
    
    private static class Wrapper
        implements Pooled
    {
        private double mValue ;
        
        public void cleanup()
        {
            mValue = Double.NaN;
        }

        public void release()
        {
            POOL.checkIn( this );
        }
    }
}

Кажется, самое простое решение - использовать BigInteger вместо long для реализации решения Песто. Если это кажется грязным, было бы легко написать класс, который обёртывает BigInteger, чтобы скрыть точность настройки.

Подобная проблема была в системе торговли акциями еще в 99 году. В самом начале разработки мы решили, чтобы каждое число в системе было представлено как длинное число, умноженное на 1000000, таким образом, 1,3423 было 1342300L. Но главным драйвером для этого был отпечаток стопы памяти, а не прямолинейность.

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

Можете ли вы дать больше информации о целях расчета?

То, что вы имеете дело с компромиссом между скоростью и точностью. Насколько велика будет потеря точности, если вы переключитесь на примитив?

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

Возможно, вы можете позволить пользователю быстро просмотреть результат, используя double, а затем запросить более точное значение, используя BigDecimal, если они пожелают?

Может быть, вам стоит взглянуть на аппаратную ускоренную десятичную арифметику?

http://speleotrove.com/decimal/

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

Вычисление bigdeciaml также дает больше / меньше копейки, рассмотрим случай 100/3.

Другие вопросы по тегам