3

我知道这个问题已经讨论过好几次了,但我对答案并不完全满意。请不要回复“双数不准确,你不能代表 0.1!你必须使用 BigDecimal”...

基本上我正在做一个财务软件,我们需要在内存中存储很多价格。BigDecimal 太大而无法放入缓存中,因此我们决定切换到 double。到目前为止,我们没有遇到任何错误,这是有充分理由的,我们需要 12 位的准确度。12 位数的估计是基于这样一个事实,即使我们以百万计,我们仍然能够处理美分。

double 给出 15 位有效十进制数字精度。如果您在必须显示/比较双打时舍入它们,会出现什么问题?

我猜问题是不准确的累积,但它有多糟糕?在影响第 12 位之前需要进行多少次操作?

你看到双打还有其他问题吗?

编辑:关于很长时间,这绝对是我们考虑过的事情。我们正在做很多除法乘法,而 long 不能很好地处理(丢失小数和溢出),或者至少你必须非常小心你所做的事情。我的问题更多是关于双打理论,基本上它有多糟糕,不准确是否可以接受?

EDIT2:不要试图解决我的软件,我对不准确很好:)。我重新提出问题:如果您只需要 12 位数字并且在显示/比较时舍入双打,那么发生不准确的可能性有多大?

4

8 回答 8

13

如果您绝对不能使用BigDecimal并且不想使用doubles,请使用longs 进行定点算术(例如,每个long值将代表美分的数量)。这将让您代表 18 位有效数字。

我会说使用joda-money,但这BigDecimal在幕后使用。


编辑(因为上面并没有真正回答问题):

免责声明: 如果准确性对您很重要,请不要double用来代表金钱。但似乎张贴者不需要精确的准确性(这似乎是关于可能具有超过 10**-12 内置不确定性的财务定价模型),并且更关心性能。假设是这种情况,使用 adouble是可以原谅的。

一般来说,adouble不能准确地表示小数。那么,a 有多不精确double?对此没有简短的回答。

Adouble可能能够很好地表示一个数字,您可以将该数字读入 a double,然后再次将其写回,保留 15 位小数的精度。但由于它是二进制而不是小数,它不可能是精确的——它是我们希望表示的值,加上或减去一些错误。当执行许多涉及不精确的算术运算时double,此错误的数量会随着时间的推移而增加,因此最终产品的精度小于十五位小数。少了多少?那要看。

考虑以下函数,它取n1000 的 th 根,然后将其乘以自身n

private static double errorDemo(int n) {
    double r = Math.pow(1000.0, 1.0/n);
    double result = 1.0;
    for (int i = 0; i < n; i++) {
        result *= r;
    }
    return 1000.0 - result;
}

结果如下:

errorDemo(     10) = -7.958078640513122E-13
errorDemo(     31) = 9.094947017729282E-13
errorDemo(    100) = 3.410605131648481E-13
errorDemo(    310) = -1.4210854715202004E-11
errorDemo(   1000) = -1.6370904631912708E-11
errorDemo(   3100) = 1.1107204045401886E-10
errorDemo(  10000) = -1.2255441106390208E-10
errorDemo(  31000) = 1.3799308362649754E-9
errorDemo( 100000) = 4.00075350626139E-9
errorDemo( 310000) = -3.100740286754444E-8
errorDemo(1000000) = -9.706695891509298E-9

请注意,累积误差的大小不会与中间步骤的数量完全成比例地增加(实际上,它不是单调增加的)。给定一系列已知的中间操作,我们可以确定不准确的概率分布;虽然这将有更广泛的操作有更多的操作,确切的数量将取决于输入到计算中的数字。不确定性本身就是不确定性!

根据您正在执行的计算类型,您可以通过在中间步骤后四舍五入到整数单位/整数美分来控制此错误。(考虑一个银行账户持有 100 美元的情况,年利率为 6%,每月复利,因此每月利息为 0.5%。在贷记第三个月的利息后,您希望余额为 101.50 美元还是 101.51 美元?double)小数单位(即美分)的数量而不是整数单位的数量会使这更容易 - 但如果你这样做,你也可以long像我上面建议的那样使用 s 。

再次声明:浮点错误的累积使得使用doubles 来表示金额可能相当混乱。作为一个多年来一直使用double十进制表示任何东西的Java开发人员,我会使用十进制而不是浮点算术来进行任何涉及金钱的重要计算。

于 2013-11-14T13:04:03.100 回答
6

Martin Fowler 就该主题写了一些东西。他建议使用内部长表示和小数因子的 Money 类。 http://martinfowler.com/eaaCatalog/money.html

于 2013-11-14T13:04:25.037 回答
6

如果不使用定点(整数)算术,您无法确定您的计算总是正确的。这是因为IEEE 754浮点表示的工作方式,一些十进制数不能表示为有限长度的二进制分数。但是,所有定点数都可以表示为有限长度整数;因此,它们可以存储为精确的二进制值。

考虑以下:

public static void main(String[] args) {
    double d = 0.1;
    for (int i = 0; i < 1000; i++) {
        d += 0.1;
    }
    System.out.println(d);
}

这打印100.09999999999859任何double使用s 的货币实施都失败。

要获得更直观的解释,请单击十进制到二进制转换器并尝试将 0.1 转换为二进制。你最终得到 0.00011001100110011001100110011001(0011 重复),将其转换回十进制你得到 0.0999999998603016138。

因此 0.1 == 0.0999999998603016138


作为旁注,BigDecimal 只是一个具有 int 十进制位置的 BigInteger。BigInteger 依靠底层 int[] 来保存其数字,因此提供定点精度。

public static void main(String[] args) {
    double d = 0;
    BigDecimal b = new BigDecimal(0);
    for (long i = 0; i < 100000000; i++) {
        d += 0.1;
        b = b.add(new BigDecimal("0.1"));
    }
    System.out.println(d);
    System.out.println(b);
}

输出:
9999999.98112945(10^8 次加法后损失一分钱)
10000000.0

于 2013-11-14T13:22:51.440 回答
2

从历史上看,使用浮点类型对可能大于 2^32 但不大于 2^52 的整数进行精确计算通常是合理的 [或者,在具有适当“long double”类型的机器上,2^ 64]。在 8088 上将 52 位数除以 32 位数以产生 20 位商需要相当长的耗时过程,但 8087 处理器可以相对快速和轻松地做到这一点。如果所有需要精确的值总是用整数表示,那么在财务计算中使用小数是完全合理的。

如今,计算机能够更有效地处理更大的整数值,因此使用整数来处理将由整数表示的量通常更有意义。浮点数对于像小数除法这样的事情似乎很方便,但正确的代码必须处理将事物四舍五入到整数的影响,无论它做什么。如果三个人需要支付 100.00 美元的费用,一个人无法通过让每个人支付 33.333333333333 美元来实现精确的会计;使事情平衡的唯一方法是让人们支付不相等的金额。

于 2013-11-21T22:14:58.017 回答
1

如果 的大小BigDecimal对于您的缓存来说太大了,那么您应该在将long它们写入缓存时将它们转换为值,并BigDecimal在读取它们时将它们转换回。这将为您的缓存提供更小的内存占用,并在您的应用程序中进行准确的计算。

即使您能够使用双精度数正确地表示您的计算输入,但这并不意味着您将始终获得准确的结果。您仍然可能遭受取消和其他事情的困扰。

如果您拒绝使用BigDecimal您的应用程序逻辑,那么您将重写许多BigDecimal已经提供的功能。

于 2013-11-14T13:16:14.063 回答
0

我将通过解决问题的不同部分来回答问题。请接受我正在尝试解决根本问题而不是状态问题。您是否查看过所有减少内存的选项?

  1. 例如,你是如何缓存的?
  2. 您是否使用 Fly Weight 模式来减少重复数字的存储?
  3. 您是否考虑过以某种方式表示常见数字?
    示例零是一个常数,零。
  4. 某种数字范围压缩或数字层次结构如何,例如主要数字的哈希映射?在标志内存储 32 位或某种类型的倍数
  5. 提示一种很酷的差异方法,http://citeseerx.ist.psu.edu/viewdoc/summary ?doi=10.1.1.65.2643
  6. 您的磨机缓存运行效率是否较低?
  7. 指针不是免费的,想过数组组吗?取决于你的问题。
  8. 您是否也将对象存储在缓存中,它们并不小,您也可以将它们序列化为结构等。

查看存储问题并停止寻找避免潜在的数学问题。通常,在您不必担心数字之前,Java 中有很多多余的东西。甚至有些你可以用上面的想法来解决它们。

于 2013-11-14T13:50:27.903 回答
-1

您不能相信财务软件中的双打。它们在简单的情况下可能工作得很好,但由于四舍五入、呈现某些值的不准确性等,你会遇到问题。

你别无选择,只能使用BigDecimal. 否则你会说“我正在编写几乎可以工作的财务代码。你几乎不会注意到任何差异。” 这不会让你看起来值得信赖。

定点在某些情况下有效,但你能确定现在和将来 1 美分的精度就足够了吗?

于 2013-11-14T13:04:57.690 回答
-1

我希望你已经阅读了 Joshua Bloch Java Puzzlers Traps Pitfalls。这就是他在谜题 2 中所说的:是时候改变了。

二进制浮点特别不适合货币计算,因为不可能将 0.1(或任何其他 10 的负幂)精确地表示为有限长度的二进制分数 [EJ Item 31]。

于 2013-11-14T13:07:32.250 回答