如何处理 Java BigDecimal 性能?

Posted

技术标签:

【中文标题】如何处理 Java BigDecimal 性能?【英文标题】:What to do with Java BigDecimal performance? 【发布时间】:2010-10-11 08:04:01 【问题描述】:

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

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

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

有什么建议吗?

【问题讨论】:

如果b和c的值变化不大,你可以记住这些值。 奇怪的是,这在 C 语言中更简单。只需链接到 BCD 库就可以了! 我记得参加过 IBM 的销售演示,内容是 BigDecimal 的硬件加速实现。因此,如果您的目标平台是 IBM System z 或 System p,您可以无缝地利用它。 一点也不奇怪,Java 使常见任务变得更容易,而大十进制并不常见。 别笑,但一种解决方案是使用 php。我刚刚在调查我从 PHP 转换为 Java 的小程序在 Java 中比 PHP 慢得多的原因时发现了这个帖子。 【参考方案1】:

我知道这是一个非常古老的线程,但我正在编写一个应用程序(顺便说一下,一个交易应用程序),其中计算诸如 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;
        
    

【讨论】:

【参考方案2】:

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

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

【讨论】:

【参考方案3】:

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

预先计算数组,这样我就不会通过生成它们来弄乱结果 永远不要致电BigDecimal.doubleValue(),因为它非常慢 不要通过添加BigDecimals 来混淆结果。只需返回一个值,并使用 if 语句来防止编译器优化。不过,请确保它在大部分时间都工作,以允许分支预测消除这部分代码。

测试:

BigDecimal:完全按照您的建议进行计算 BigDecNoRecip: (1/b) * c = c/b, 只做 c/b 双打:用双打做数学

这是输出:

 0% Scenariovm=java, trial=0, benchmark=Double 0.34 ns; ?=0.00 ns @ 3 trials
33% Scenariovm=java, trial=0, benchmark=BigDecimal 356.03 ns; ?=11.51 ns @ 10 trials
67% Scenariovm=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]);
  

【讨论】:

+1 用于制定基准,但 -1 用于实施。您主要测量创建 BigDecimal... 需要多长时间,或者更准确地说,创建开销存在于所有基准测试中,并且可能占主导地位。除非这是您想要的(但为什么?),您需要预先创建值并存储在数组中。 @maaartinus 嗯,这很尴尬,在过去的 14 个月里,我在编写基准测试方面已经变得如此了。我现在将编辑帖子 +1 现在这些值是有意义的!我不确定你在用if 做什么。它可能不会被优化,但它可能会。我曾经做过result += System.identityHashCode(o) 之类的事情,但后来我发现了JMH BlackHole @maaartinus 你能告诉我更多关于 JMH 黑洞的信息吗? @AmrinderArora 不是。 Blackhole 是一个非常复杂的东西,对输入做一些事情,所以它不能被优化掉。即使在多线程情况下,它也针对速度进行了优化。【参考方案4】:

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

【讨论】:

【参考方案5】:

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

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

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

【讨论】:

【参考方案6】:

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

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

【讨论】:

舍入结果降低准确性,而不是提高准确性。 @Hannele 大多数时候是的,但有时确实会增加它。例如,当计算价格总和时,每个价格都带有两位小数,四舍五入到两位小数保证是正确的结果(除非您将数十亿的值相加)。跨度> @maaartinus 你有一个有趣的观点!但是,我不认为这直接适用于 OP(部门)。 @Hannele:同意,只有当您知道结果应该有多少位小数时,舍入才有帮助,而除法不是这种情况。 如果double 值的缩放方式使得任何域所需的舍入始终为整数,那么任何舍入的值都将是“精确的”,除非它们真的很大。例如,如果将四舍五入到最接近的 0.01 美元的东西存储为便士而不是美元,double 将可以精确地四舍五入的金额,除非它们超过 45,035,996,273,704.96 美元。【参考方案7】:

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

【讨论】:

【参考方案8】:

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

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

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

再次编辑:您可能会发现 BigInteger 可以满足您的需要。

【讨论】:

在这种情况下你能给我推荐一个除法算法吗?【参考方案9】:
Only 10x performance increase desired for something that is 1000x slower than primitive?!.

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

【讨论】:

【参考方案10】:

大多数双重运算都可以为您提供足够的精度。您可以用 double 精确表示 10 万亿美元,这对您来说可能绰绰有余。

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

【讨论】:

是的,double的精度绰绰有余。我也做这样的事情,除非我忘记四舍五入,否则客户会看到类似 -1e-13 的东西,他们期望得到非负结果。 我已经为不同的基金设计了三种不同的交易系统,并使用double 获取价格或long 美分。【参考方案11】:

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

请注意,我今天不会再这样做了,除非我真的确定数学表现非常关键。在大多数标准 Web 应用程序中,jdbc 访问和访问其他网络资源的开销超过了快速计算的任何好处。

【讨论】:

【参考方案12】:

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

http://speleotrove.com/decimal/

【讨论】:

【参考方案13】:

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

也许http://gmplib.org/

【讨论】:

JNI 不太可能帮助提高性能,除非可以批量计算。当您跨越 JVM/本机边界时,JNI 会引入大量开销。 你说得对,边界确实减速了,我确实感受到了这种痛苦,但如果 BigDecimal 真的有声称的 1000 倍减速,而 JNI 只是一小部分,那可能是值得的。跨度> 【参考方案14】:

就我个人而言,我认为 BigDecimal 不适合这个。

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

【讨论】:

【参考方案15】:

您使用的是什么版本的 JDK/JRE?

您也可以尝试ArciMath BigDecimal 看看他们是否为您加快了速度。

编辑:

我记得在某处(我认为是 Effective Java)读到 BigDecmal 类在某个时候从被 JNI 调用到 C 库更改为所有 Java ......并且它变得更快。因此,您使用的任何任意精度库都可能无法获得所需的速度。

【讨论】:

【参考方案16】:

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

您是否使用 org.jscience.economics.money 进行了测试?因为这保证了准确性。固定点的准确度与分配给每个部分的位数一样,但速度很快。

【讨论】:

JScience 是优秀的库,我必须承认;但是,与 BigDecimal 相比,性能并没有提升。 使用定点库可以提高速度,但会损失一些精度。您可以尝试使用 BigInteger 制作定点库。 也不要使用 10 的幂,如果这样做,请使用 2 的幂。10 的幂对人类更容易,但对计算机更难:P【参考方案17】:

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

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

【讨论】:

我不需要精确的表示;我需要可知的精度。【参考方案18】:

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

【讨论】:

这增加了更多的操作。这样会慢一些。 怎么慢? long 上的数学计算比 BigDecimal 上的要快得多。您只需要转换为美元和美分即可输出。 我需要跟踪(在中间计算中)十亿分之一的美分。假设我们有一个美元/日元的报价:99.223。在其他地方我需要 JPY/USD 报价,大约是 0.0100779022(我需要更精确)。 @Pesto:错过了长转换,然而,2 个小数点在货币计算中几乎是不可接受的,尽管类似于我对定点数学的建议。 @Pesto:是的,一个基元是不够的,这就是我建议使用定点库的原因。【参考方案19】:

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

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

【讨论】:

自己从头开始实现除法、舍入、取幂等? :) 是的,我相信这就是他的建议。 这是一项非常困难的任务(如果您有疑问,请查看 Java Math 类)。我不相信没有其他人在 Java 中进行高性能货币计算。 对于一个通用库来说,这是一项艰巨的任务。对于特定的应用程序(仅使用 子集)的操作来说,这是微不足道的。其实我自己的app里就有这样的class,只需要5、6个常用操作。 如果您以编写货币交易应用程序为生,这些计算就是您的“核心功能”。您将需要花费时间和精力使它们正确,从而为自己带来竞争优势。

以上是关于如何处理 Java BigDecimal 性能?的主要内容,如果未能解决你的问题,请参考以下文章

java是如何处理异常的?

Java虚拟机是如何处理异常的?

Java虚拟机是如何处理异常的?

Socksifying Java ServerSocket - 如何处理

Simple JavaJava是如何处理别名(aliasing)的

javamail如何处理退信