【问题标题】:Using Double for financial Software [closed]在财务软件中使用 Double [关闭]
【发布时间】:2013-11-14 13:00:30
【问题描述】:

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

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

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

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

你发现双打还有其他问题吗?

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

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

【问题讨论】:

  • 你不能只存储全部的美分吗?
  • @tibo 这就是我所说的 - “你的方法的内存复杂性有问题”。即使你保存了这 24 个字节,当你有 4000 万而不是 10 个价格时,它仍然会爆炸......
  • 关于双打是“坏”的,见my answer;基本上双打并不比我们自己的十进制系统差(尝试用十进制表示 1/3)。这两个系统只是具有他们“喜欢”的不同数字。在这种情况下,您应该考虑双打。金融系统特别喜欢 1/100,它恰好可以用十进制精确表示,但不能用二进制表示
  • 这有点像 hack,但如果你必须必须使用双精度数,那么每次操作都会四舍五入到 2 位有效数字会抑制错误的累积
  • 问题“在影响第 12 位之前需要进行多少次操作?”没有其他信息就无法回答,特别是要执行哪些操作以及使用什么值,特别是因为问题提到了除法和乘法,而不仅仅是加减金钱。

标签: java floating-point double bigdecimal ieee-754


【解决方案1】:

如果您绝对不能使用BigDecimal 并且不想使用doubles,请使用longs 来执行fixed-point arithmetic(因此每个long 值将代表美分的数量,例如)。这将让您代表 18 位有效数字。

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


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

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

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

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

考虑以下函数,它取 1000 的 nth 根,然后将其乘以 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 美元?)拥有您的 @987654340 @ 代表小数单位的数量(即美分)而不是整数单位的数量会使这更容易 - 但如果你这样做,你也可以像我上面建议的那样使用longs。

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

【讨论】:

  • 看可以编辑,这是一个很好的观点,但那不是我要找的;)
  • 考虑到百分比利率,这仍然是一个问题(1 * 1.05 是 1.05,qhich 是 1.05 美分,仅此而已)
  • 扩展以尝试更好地回答原始问题。
  • 真的很喜欢你的代码示例,这是一个很好的开始,可以粗略地估计不准确的累积有多严重。注意,您可以使用 BigDecimal 进行相同的推理(并将它们除以 3 或 7 或 9 或....)。关于 long 我认为这是最糟糕的解决方案,将其除以 3,结果会被截断,将 2 long 相乘,最终会出现长溢出...
【解决方案2】:

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

【讨论】:

  • 包装类 对象指针开销 我的缓存大小至少乘以 2
  • 是的,但是您可以获得更可靠的代码、验证和其他内容。
  • 另一方面,性能和内存使用越来越差。取决于你想使用它的方式。对于处理数百万数量的服务器来说,这不是一个好主意 - 在 GUI 中,它的内置验证可以提供很多帮助,例如防止用户将混合货币放在一个包中或在日元上使用小数...
  • Money 类听起来没有必要,它类似于 BigDecimal 的实现。一个 BigInteger(无限精度的非小数整数,用 char[] 完成)和一个小数位置值。
  • 如果您只想存储金额,它不会添加任何内容。但是,如果您使用不同的货币,您可以实施完整性检查或使用允许的确切小数位数。例如。日元没有“分”。如果你不这样做,你最终可能会得到半日元。
【解决方案3】:

如果不使用定点(整数)算术,您将无法确定您的计算总是正确的。这是因为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.09999999999859ANY 使用doubles 实施金钱失败。

如需更直观的解释,请单击decimal to binary converter 并尝试将 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

【讨论】:

  • 我的意思是精度并不重要。即使有 150 位有效数字精度,在 10000 次加法后也会出错。在这种情况下,任何浮点类型都无法成功使用。
  • @tibo 等到您的软件已经运行多年并且硬币开始出现/丢失。
  • @tibo 好的,我想我明白你的意思了。不过请考虑 BartoszKP 的评论:如果将空间复杂度增加不到 4 倍超出了您的限制,那么您可能正在寻找错误的解决方案类型。
  • @RonE 关于你的旁注,你用浮点数初始化你的 BigDecimal,所以你有不准确的地方。用字符串“0.1”初始化它,我很确定问题会消失;)
  • 第一条语句,即“不使用定点(整数)算术,你永远无法确定你的计算是正确的”,是错误的。 IEEE-754 规范明确,易于数学证明。对于特定情况,可以设计产生正确结果的计算,并且可以证明是这样。 .01 没有精确表示这一事实并不意味着不可能设计出暂时包含不准确但其结果设计得足够好以至于最终可以产生精确值的计算。
【解决方案4】:

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

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

【讨论】:

    【解决方案5】:

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

    即使您能够使用双精度数正确地表示您的计算输入,但这并不意味着您将始终获得准确的结果。你仍然可以忍受cancellation 和其他东西。

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

    【讨论】:

    • 在将 BigDecimals 转换为 long 或反之时如何保证不丢失精度?
    • @LionC 您可以用单个longLONG.MIN_VALUE 美分表示为Long.MAX_VALUE 美分金额。如果应用程序将存储在缓存中的所有数量都在该时间间隔内,则此方法有效。
    • 但是你不能存储半分钱等等,考虑到利率税等等是财务软件中的一个问题
    • 实际上@SpaceTrucker 这是一个很好的观点,我认为我应该这样做(在实现中很长并且返回 BigDecimal 的 getter)。
    【解决方案6】:

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

    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 中有很多多余的东西。甚至有些你可以用上面的想法来解决它们。

    【讨论】:

    • 感谢您的回答泰德。没想到会这样,但很聪明。实际上是的,在我们的缓存中,我们有不同类型的优化,包括压缩和使用共享值池。我们存储的对象很少,例如我们使用 Trove 来收集原始数据。老实说,现在我们已经不再关注它了,因为我们现在已经达到了我们所需要的。
    • 感谢您本着所提供的精神接受它。干杯。
    【解决方案7】:

    您不能相信财务软件中的替身。它们在简单的情况下可能效果很好,但由于四舍五入、表示某些值的不准确等,您会遇到问题。

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

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

    【讨论】:

    • 在我的情况下,不准确是可以接受的,但这不是重点。我试图找出 double 的限制,以及这种不准确性在什么时候变得可见
    • @tibo 答案是:视情况而定。你不能依赖 double 准确到某个点。如果你做很多乘法和除法,最终你会得到错误的数字。那么你就必须希望你的客户没有注意到它。
    • 我认为你错过了我的观点。我知道你必须处理不准确的问题。 BigDecimal 也是如此,例如你不能代表 1/3,你也会像你说的那样想出错误的数字。一切都是关于处理不准确的。 BigDecimal 有 MathContext 而双打有默认机制。看到每个人都认为 BigDecimal 是灵丹妙药而没有深入研究,我真的很生气。 BigDecimal 很棒,但针对不同的问题有不同的解决方案
    • @tibo 到目前为止,您还没有提出任何不同的解决方案。您说过“我不想使用 BigDecimal,但我想要它提供的功能”。 BigDecimal 和 double 中的不准确性之间的区别在于,在 BigDecimal 中,决定何时实现不准确性(即计算完成时),而在 double 中,您永远不知道是否准确性已经下降到地狱,而你只会让它变得更糟。
    • 此外,我们对您的解决方案一无所知。你可能有一个完全愚蠢的实现,而不是解决根本原因,你责怪BigDecimal。如果您正在制作专业的财务软件,我无法相信您会因为 BigDecimals 而耗尽内存!
    【解决方案8】:

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

    二进制 浮点数特别不适合货币计算,因为它无法表示 0.1 — 或任何其他 10 的负幂 — 完全作为有限长度二进制分数 [EJ Item 31]。

    【讨论】:

    • 我喜欢这个解释。它显示了不仅要表示多少位数,而且一些有理十进制数不能表示为任何有限长度的二进制分数
    • 我知道这一点,我已经阅读了很多次这个答案......我正在努力超越这一点
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2010-10-22
    • 1970-01-01
    • 1970-01-01
    • 2011-03-05
    • 2010-12-04
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多