Java 中的定点算术,高效实现

6
我需要在Java中以完美精度和小数点后固定的数字表示一些数字;小数点后面的部分我不关心。(更具体地说 - 货币和百分比。)
我现在使用Java自带的BigDecimal,但我发现它非常慢,在我的应用程序中开始表现出来。
因此,我想用“常规”整数和定点算术来解决它(长整数对于我的目的具有足够的精度)。
现在,我认为我不是第一个遇到这种问题的人,已经有一个库可以解决这个问题,并且已经实现了乘法/除法 - 但似乎并没有。
现在,我很可能可以自己编写它(而我可能会这样做),但真的,我真的是第一个需要这个的人吗?难道没有已经存在的库吗?

1
%100 的精度?这是无限位数的数字。 - huseyin tugrul buyukisik
你需要保留小数点后几位数字? - huseyin tugrul buyukisik
抱歉,我需要用比喻的方式表达100%。我会添加更多信息。 - Karel Bílek
可能是 https://dev59.com/xXVC5IYBdhLWcg3wfhKL 的重复问题。 - Raedwald
4个回答

10

decimal4j是一个Java库,用于快速处理长整型的固定精度算术运算,并支持高达18位小数。

声明:我参与了decimal4j项目。


6
您是否完全确定BigDecimal是性能问题?您是否使用分析工具进行了分析?如果是的话,有两个选项可以帮助您:
1)使用 long 并将所有值乘以一个因子(例如,如果您想使用分为单位,则为100)。
2)使用特别设计的类来实现类似于 BigDecimal 的功能,但在内部使用 long 。我不知道是否存在好的开源库(也许是Java Math Fixed Point Library?)。我自己在很长时间前(2001年,我相信)为J2ME编写了这样的类。这有点棘手,请注意 BigDecimal 也会在需要高精度时在内部使用long,因此在大多数情况下,此解决方案只会稍微有所帮助。
在许多情况下,使用 double 不是一个好选择,因为存在舍入和精度问题。

+1 所有问题都可以解决,舍入误差不是随机误差并且很容易解决。在我看来,如果更简单的话,应该使用 long,否则如果你有非常大的数字,应该使用 doubleBigDecimal - Peter Lawrey
1
使用一个专门设计的类,它实现了类似于 BigDecimal 的功能,但内部使用长整型。除非需要膨胀,否则 BigDecimal 已经可以胜任(即 long 不够用)。 - bestsss
BigDecimal 内部不使用 long,而是使用 BigInteger,而 BigInteger 又在内部使用了一个 int[]。这就是为什么使用 BigDecimal 比使用 long 稍微慢一点的原因。然而,在许多(大多数)情况下,性能差异太小,不重要。 - Thomas Mueller
@Karel,如果你有很多对象,对象的创建仍然可能是一个问题,但我确实怀疑这不是真正的瓶颈。(对于BigDecimals的GC来说,它们在young gen中死亡是微不足道的) - bestsss
@bestsss 你说的BigInteger使用long是正确的!我之前不知道这个优化...非常感谢!总有新东西需要学习。 - Thomas Mueller
显示剩余3条评论

0

不确定为什么需要一个库。

例如,假设您想要使用相同的固定精度添加两个长整型数。

long c = a + b;

假设你有一个固定精度的数字,你想要将其乘以一个整数。
long c = a * i;

假设你想将一个数除以一个整数并向零舍入

long c = a / i;

假设你想要打印一个保留3位小数的固定精度数字。

System.out.println(c / 1e3);

也许你正在过度思考问题,并假设你需要为所有事情都使用库。
如果你正在使用 long 或 double,你可能需要一些小的数字辅助方法来进行四舍五入,但并不需要像这样的库。

3
-1图书馆会很有用,3小数位示例:1.234 * 1.234是1234 * 1234 = 1522756,你必须除以1000,否则你得到的是1522.756,而不是正确的1.522。 A library would be helpful. For example, if you perform a calculation such as 1.234 * 1.234, the result is 1234 * 1234 = 1522756. However, you need to divide by 1000 to get the correct result, which is 1.522 instead of 1522.756. - peenut
1
@Peenut 写一个帮助类来满足你的应用程序需求是如此微不足道,但我从未费心去做过,也从未见过有人这样做。不确定为什么会这样。 - Peter Lawrey
1
让我引用更好的答案:“这有点棘手。” - peenut
2
最糟糕的部分���处理算术溢出。假设我们需要跟踪5个小数位(例如,以1/10个点跟踪货币对价格)。两个定点数(例如,价格*大小)的乘法很容易导致溢出,即使小数点后只有100K。 - Andy Malakov
@AndyMalakov 我同意,我的偏好是使用 double,它既不会对正常数字溢出,也不会因为使用固定精度而产生错误的数量级。虽然 double 可以产生表示误差的奇怪数字,但至少它们显然很奇怪(但相对较小的误差)。如果固定精度或 BigDecimal 是错误的,它仍然看起来很好。 - Peter Lawrey
1
认真看一下 Spire (https://github.com/non/spire)。它需要使用 Scala 而非 Java,但它具有非常有趣的功能集。 - James Moore

0

虽然这不完全是你所询问的,但它可以加速你的应用程序而不离开BigDecimal

自Java 8以来,BigDecimal本身已经解决了这个问题。添加了一个新类MathContext,限制了计算操作的精度。

var num = new BigDecimal("1234.56780", new MathContext(10, RoundingMode.DOWN));

问题在于精度10不适用于小数点后的数字,而是适用于有效数字的数量。对于1234.50,需要6个有效数字。
对于1_500_000_000.100,需要13个有效数字才能保持数字不变。
因此,当您拥有10位精度并计算数十亿捷克克朗时,精度可能会受到影响。
但是,例如1000的精度比无限精度(我认为这是默认值)要快得多。

这也可以应用于单个操作:

BigDecimal n = new BigDecimal("0.12345");
n = n.pow(2, new MathContext(1000, RoundingMode.DOWN));
n = n.pow(2, new MathContext(1000, RoundingMode.DOWN));
n = n.pow(2, new MathContext(1000, RoundingMode.DOWN));
n = n.pow(2, new MathContext(1000, RoundingMode.DOWN));

这里有一个关于任意精度数字的更长解释 - Ondra Žižka

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接