62

我为生活编写货币交易应用程序,所以我必须使用货币值(遗憾的是,Java 仍然没有十进制浮点类型并且没有任何东西支持任意精度的货币计算)。“使用 BigDecimal!” ——你可能会说。我愿意。但是现在我有一些代码,其中性能一个问题,BigDecimal 比double原语慢 1000 多倍(!)。

计算非常简单:系统所做的是a = (1/b) * c多次计算(其中和a是定点值)。然而,问题在于这个。我不能使用定点算术,因为没有定点。而且不仅丑陋,而且速度缓慢。bc(1/b)BigDecimal result = a.multiply(BigDecimal.ONE.divide(b).multiply(c)

我可以用什么来代替 BigDecimal?我需要至少 10 倍的性能提升。我发现了其他优秀的 JScience 库,它具有任意精度的算术,但它甚至比 BigDecimal 还要慢。

有什么建议么?

4

19 回答 19

39

也许您应该从用 a = c/b 替换 a = (1/b) * c 开始?它不是 10 倍,但仍然是一些东西。

如果我是你,我会创建自己的 Money 类,它会保留多头美元和多头美分,并在其中做数学运算。

于 2009-03-04T18:02:07.080 回答
18

大多数双重操作为您提供了足够的精度。你可以用双精度表示 10 万亿美元,这对你来说可能绰绰有余。

在我工作过的所有交易系统(四家不同的银行)中,他们都使用双精度和适当的四舍五入。我认为没有任何理由使用 BigDecimal。

于 2009-03-04T19:26:33.117 回答
15

所以我最初的答案是完全错误的,因为我的基准写得不好。我想我是应该受到批评的人,而不是 OP ;) 这可能是我写过的第一个基准测试之一……哦,好吧,这就是你学习的方式。而不是删除答案,这里是我没有测量错误的结果。一些注意事项:

  • 预先计算数组,这样我就不会通过生成它们来弄乱结果
  • 永远不要打电话BigDecimal.doubleValue(),因为它非常慢
  • 不要通过添加BigDecimals 来弄乱结果。只需返回一个值,并使用 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]);
  }
}
于 2013-02-06T16:23:26.353 回答
9

假设你可以工作到任意但已知的精度(比如十亿分之一美分)并且有一个你需要处理的已知最大值(一万亿美元?),你可以编写一个类,将该值存储为十亿分之一的整数一分钱。你需要两个 long 来表示它。这应该比使用 double 慢十倍;大约是 BigDecimal 的一百倍。

大多数操作只是对每个部分执行操作并重新归一化。除法稍微复杂一些,但并不多。

编辑:回应评论。您将需要在您的类上实现位移操作(很容易,因为高 long 的乘数是 2 的幂)。要进行除法,请移动除数,直到它不大于被除数;从被除数中减去移位的除数并增加结果(使用适当的移位)。重复。

再次编辑:您可能会发现 BigInteger 在这里可以满足您的需求。

于 2009-03-04T18:23:44.657 回答
5

将多头存储为美分数。例如,BigDecimal money = new BigDecimal ("4.20")变成long money = 420。您只需要记住修改 100 即可获得美元和美分的输出。如果你需要跟踪,比如说,十分之一美分,它会变成long money = 4200

于 2009-03-04T18:06:19.347 回答
5

您可能想转向定点数学。现在只是在搜索一些图书馆。在 sourceforge定点上,我还没有深入研究过这个。熊熊的

你用 org.jscience.economics.money 测试过吗?因为这保证了准确性。固定点将仅与分配给每个部分的位数一样准确,但速度很快。

于 2009-03-04T18:09:59.633 回答
3

就个人而言,我不认为 BigDecimal 是理想的。

您确实想在内部使用 long 来实现您自己的 Money 类,以表示最小单位(即美分、10 美分)。在这方面有一些工作,实施add()等等divide(),但这并不是那么难。

于 2009-03-04T19:12:23.400 回答
2

What version of the JDK/JRE are you using?

Also you might try ArciMath BigDecimal to see if theirs speeds it up for you.

Edit:

I remember reading somewhere (I think it was Effective Java) that the BigDecmal class was changed from being JNI called to a C library to all Java at some point... and it got faster from that. So it could be that any arbitrary precision library you use is not going to get you the speed you need.

于 2009-03-04T18:40:22.150 回答
2
Only 10x performance increase desired for something that is 1000x slower than primitive?!.

在这方面投入更多的硬件可能会更便宜(考虑到货币计算错误的可能性)。

于 2009-06-29T05:00:59.660 回答
1

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或其他任何东西。我想我会提到这一点,因为它有点令人困惑。

于 2013-09-12T15:58:16.473 回答
1

1/b 也不能完全用 BigDecimal 表示。请参阅 API 文档以了解结果是如何四舍五入的。

围绕一两个长字段编写自己的固定十进制类应该不会太难。我不知道任何合适的现成库。

于 2009-03-04T18:09:59.727 回答
1

我知道我在非常老的主题下发帖,但这是谷歌发现的第一个主题。考虑将您的计算移至您可能从中获取数据进行处理的数据库。我也同意 Gareth Davis 的观点,他写道:

. 在大多数沼泽标准 webapps 中,jdbc 访问和访问其他网络资源的开销淹没了真正快速数学的任何好处。

在大多数情况下,错误查询比数学库对性能的影响更大。

于 2012-06-18T10:41:23.477 回答
1

在 64 位 JVM 上,按如下方式创建 BigDecimal 使其速度提高约 5 倍:

BigDecimal bd = new BigDecimal(Double.toString(d), MathContext.DECIMAL64);
于 2017-04-27T19:58:24.617 回答
0

早在 99 年的股票交易系统中就有类似的问题。在设计之初,我们选择将系统中的每个数字表示为 long 乘以 1000000,因此 1.3423 是 1342300L。但主要驱动因素是内存占用而不是直线性能。

一句话提醒,除非我真的确定数学表现非常关键,否则我今天不会再这样做了。在大多数标准 webapps 中,jdbc 访问和访问其他网络资源的开销淹没了快速计算的任何好处。

于 2009-03-11T20:18:50.280 回答
0

容易...对您的结果进行四舍五入通常会消除双重数据类型的错误。如果您正在计算余额,您还必须考虑谁将拥有四舍五入造成的更多/更少便士。

bigdeciaml 计算也会产生更多/更少的便士,考虑 100/3 的情况。

于 2011-05-04T16:20:52.123 回答
0

JNI有可能吗?您可能能够恢复一些速度并可能利用现有的本机定点库(甚至可能还有一些 SSE* 优点)

也许http://gmplib.org/

于 2009-03-04T19:08:00.227 回答
0

也许您应该考虑获得硬件加速的十进制算术?

http://speleotrove.com/decimal/

于 2009-03-04T19:25:17.567 回答
0

似乎最简单的解决方案是使用 BigInteger 而不是 long 来实现pesto 的解决方案。如果它看起来很乱,那么编写一个包装 BigInteger 的类来隐藏精度调整会很容易。

于 2010-10-30T05:19:42.200 回答
0

我知道这是一个非常古老的线程,但我正在编写一个应用程序(顺便说一下,一个交易应用程序),其中在几千个历史烛台上计算 MACD 等指标(计算多个指数移动平均线)的数量令人无法接受时间(几分钟)。我正在使用 BigDecimal。

每次我滚动或调整窗口大小时,它都必须遍历缓存值以调整 Y 比例,但即使这样也需要几秒钟才能更新。它使应用程序无法使用。每次我调整各种指标的参数时,重新计算都需要几分钟。

然后我把它全部改成双倍,它的速度要快得多。问题是我使用哈希图缓存值。我想出的解决方案使用一个双值的包装池。通过合并包装器,您不会受到自动装箱到/来自 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
        {
            return lWrapper.mValue;
        }
    }
    
    public double remove( K pKey )
    {
        Wrapper lWrapper = mMap.remove( pKey );
        if( lWrapper != null )
        {
            double lDouble = lWrapper.mDouble;
            POOL.checkIn( lWrapper );
            return lDouble;
        }
        else
        {
            return Double.NaN;
        }
    }

    private static class Wrapper
        implements Pooled
    {
        private double mValue ;
        
        public void cleanup()
        {
            mValue = Double.NaN;
        }
    }
}
于 2021-06-25T16:49:45.590 回答