Skip to content

设计 Precision 浮点数精度

Baoying Wang edited this page May 10, 2020 · 2 revisions

介绍

对于金融系统来说,精度问题需要重视,否则可能会出现帐目不平的问题(如多出来1分钱,怎么解释呢?)。但是只要使用/设计时,小心一点,还是可以做到不出问题。 为什么精读可能引起问题,这是由于几乎所有语言都是用IEEE754(https://en.wikipedia.org/wiki/IEEE_754)方式来存储浮点数。此方式无法准确表达很多小数。 可以搜索一下,关于这个问题的讨论不计其数(不管是国内还是国外), 如

一般来说,我见到的文章都会说两点 1. IEEE754导致浮点数问题啦,要注意啊,但是很少说该怎么做。 2. 请不要直接比较两个浮点数是否相等(而是采用差的绝对值小于很小的数).这是很重要,但是对于系统设计开发来说还不够.

别人怎么说

"In all the trading systems I have worked on (four different banks), they have used double with appropriate rounding. I don't see any reason to be using BigDecimal." at Apr 25 '09 at 0:59 and "I have since designed three different trading systems for different funds and used double for prices or long cents." at Aug 5 '12 at 6:50 , both by Peter Lawrey from [Ref 1 - java big decimal perf]

Lesson learn : "Had a similar problem to this in an equity trading system back in 99. At the very start of the design we choose to have every number in the system represented as a long multiplied by 1000000 thus 1.3423 was 1342300L. But the main driver for this was memory foot print rather than straight line performance. One word on caution, I wouldn't do this again today unless I was really sure that the math performance was super critical. In most bog standard webapps the overhead of jdbc access and accessing other network resources swamps any benefit of having really quick math." by Gareth Davis at Mar 11 '09 at 20:18 from [Ref 1 - java big decimal perf]

一般怎么做

基本上,两种解决方式

  1. 消除小数。使用最小货币方式,如人民币业务精确到分。但是这种方式有两个问题a) 代码复杂引入很多 b) 有时候需要货币间转换,无法满足
  2. 使用过程中,注意精度。问题:如果某些地方没有进行必要的round,可能引起问题。本文将描述这种方式,我的matching engine中也将使用这种方式。

另外:有些人可能会说,我使用java 的big decimal不就解决问题了么? 能够想到使用big decimal是个好的开始。但是这还只是一小步,因为还涉及到1) 接收到客户的数据精度, 2)发送给客户的精度, 3)保存在数据库中(以及从数据库中load)的数据精度处理问题。

我的建议

总之是一句话,application runtime时候,要明确精度已经是符合业务需求的,建议使用BigDecimal,如果你没有其他特别的业务处理。

Application Runtime内部表示 - Big Decimal or double

有人说如果系统对performance要求很高,而且数据处理的量很大,不要使用BigDecimal了,以避免内存开销。 但是我暂时没有证据支持这种说法。而且,我觉得java的young gen gc还是很快的。而且我们是无法避免完全不使用BidDecimal的,因为在处理输入/输出数据时,需要使用。见下文。

我个人还是建议直接使用BigDecimal的,因为它确实简化了内部处理。但是缺点是其同时一些内部计算代码很麻烦。但是,如果有复杂的计算,可以使用double来计算,然后将计算结果round一下保存为BigDecimal.

数据库保存,以及从数据库load的数据是否需要在代码中round

  • save to db: 不需要: 如果数据库中使用浮点数类型,并且指定了小数位数(请这么做). 这样保存数据时,自动round。
  • load to app:如果内部使用BigDeimal,数据库如果使用精确精度,譬如mysql的decimal(m,n), oracle的number(m,n),精度是没有丢失的。

如何使用浮点数

总结一下就是:从外部获得的数据,要先round后再处理. 发送到外部接口的数据要先round再发送。

例如

  1. 从客户处拿来的浮点数,需要round之后再使用(不管你使用的是BigDecimal 还是 double) 客户的数据可以通过多个方式到达我们的系统。但是基本都是网络数据流的方式。拿到数据以后,将数据进行统一的round,可以保证我们的所有数据的值的一致性,不会出现3.000000000000001, 和2.99999999999999999的情况,因为我们的round一般会是6位,甚至10位,都可以避免这个问题。 例如FIX或Jason,获取字符串然后build BigDecimal再round。需要转换为double的话,使用BigDecimal。见:https://stackoverflow.com/questions/22036885/round-a-double-in-java

  2. 发送给客户的浮点数先round一下,以保证一致性。 例如FIX或者Jason,发double或者BigDecimal round为符合精度长度的字符串。见:https://stackoverflow.com/questions/153724/how-to-round-a-number-to-n-decimal-places-in-java (使用期望的java.math.RoundingMode, 文中为Ceiling )

注意 - BigDecimal的 2.0 与 2.00 - does not equal. false :new BigDecimal("2.0").equals(new BigDecimal("2.00"))

注意 - 请严格区分安全(round 过的)与非安全(未round过)的浮点数

可以使用变量名特别标注。参考Joe的那个关于安全数据的命名方式(哪本书来着?什么启示录?)