十进制 vs 双精度浮点数速度

57

我编写金融应用程序,经常纠结使用double还是decimal。

我的所有数学计算都在不超过5位小数且不大于100,000的数字上进行,我有一种感觉这些数字无论如何都可以用双精度表示而不会出现舍入误差,但从来没有确定过。

我会转换为双精度以获得明显的加速优势,但最终,我仍然使用ToString方法将价格传输到交易所,并需要确保它始终输出我期望的数字 (例如89.99,而不是89.99000000001)。

问题:

  1. 速度优势是否真的像简单测试所示的那么大? (~100倍)
  2. 有没有一种方法可以保证ToString的输出结果是我想要的?我的数字总是可表示的,这是否可以保证?

更新:在我的应用程序运行之前,我必须处理约100亿个价值更新,并且现在已经实现了使用小数点以保护数据的明显原因,但开机时间需要大约3个小时,使用双精度将极大地缩短开机时间,有没有安全的方法可以使用双精度?


1
你的瓶颈在哪里?你的CPU周期都在哪里消耗?如果没有可靠的测量数据,很有可能你正在看错问题。 - Jonathan Allen
4
因为十进制的分母是10的幂次方,而二进制的分母是2的幂次方,所以即使有一位小数,也无法在二进制中精确表示。例如,0.1(十分之一)在二进制中没有精确的等价物,这与三分之一在十进制中没有精确表示的原理相同。 - Matt Curtis
7个回答

86
  1. 浮点运算通常会更快,因为硬件直接支持它。到目前为止,几乎没有广泛使用的硬件支持十进制运算(虽然这种情况正在改变,请参阅注释)。
  2. 金融应用程序应该始终使用小数,由于在金融应用程序中使用浮点数而导致的恐怖故事不胜枚举,您可以通过谷歌搜索找到许多这样的例子。
  3. 尽管十进制运算可能比浮点运算慢得多,但除非您花费大量时间处理十进制数据,否则对程序的影响可能微乎其微。像往常一样,在开始担心差异之前,请进行适当的剖析。

15
在进行优化之前,避免过早优化。先进行测量,只有在获得证据后再进行优化。 - S.Lott
1
没有广泛使用的硬件支持,IBM大型机(仍然非常受欢迎)具有硬件十进制。 - S.Lott
4
支持新的 IEEE 754 浮点标准的新机器将在硬件上支持十进制算术运算。我相信 IBM Power6 就是其中之一。 - Jonathan Leffler
3
几乎没有被广泛使用的硬件。 - Robert Gamble
@Jonathan:我知道IBM最近在Power6中宣布了十进制支持,并且预计将在其他系列中集成支持,其他公司可能会跟随这一趋势,这是一件好事,但是在硬件中广泛使用十进制支持还需要很多年时间。 - Robert Gamble
我曾经在一个系统上工作过,其中十进制性能是瓶颈。这是一个基于Windows的夜间批处理过程。时间是1996年。 - sal

27

这里有两个可分的问题。一个是双精度是否具有足够的精度来保存您所需的所有位,另一个是它能否准确地表示您的数字。

至于精确表示,您很明智地保持谨慎,因为像1/10这样的十进制小数没有精确的二进制对应物。但是,如果您知道您只需要5个十进制数字的精度,您可以使用比例缩放算法,其中您操作被乘以10^5的数字。所以例如,如果您想要准确表示23.7205,您将其表示为2372050。

看看是否有足够的精度:双精度浮点数给您53位的精度,这相当于15+个十进制数字的精度。因此,这将允许您在小数点后五个数字和小数点前十个数字之间进行选择,在您的应用程序中似乎非常充分。

我会将这段C代码放在.h文件中:

typedef double scaled_int;

#define SCALE_FACTOR 1.0e5  /* number of digits needed after decimal point */

static inline scaled_int adds(scaled_int x, scaled_int y) { return x + y; }
static inline scaled_int muls(scaled_int x, scaled_int y) { return x * y / SCALE_FACTOR; }

static inline scaled_int scaled_of_int(int x) { return (scaled_int) x * SCALE_FACTOR; }
static inline int intpart_of_scaled(scaled_int x) { return floor(x / SCALE_FACTOR); }
static inline int fraction_of_scaled(scaled_int x) { return x - SCALE_FACTOR * intpart_of_scaled(x); }

void fprint_scaled(FILE *out, scaled_int x) {
  fprintf(out, "%d.%05d", intpart_of_scaled(x), fraction_of_scaled(x));
}

可能还有一些小缺陷,但这应该足以让您开始。

添加没有开销,乘法或除法的成本翻倍。

如果您可以访问C99,还可以尝试使用int64_t 64位整数类型进行比例整数算术运算。哪个更快将取决于您的硬件平台。


这里的乘法和除法有什么限制? - Joe
1
很难相信这个回答没有得到更高的投票,因为它是对于寻求基于性能瓶颈观察的速度改进的OP来说更好的答案。缩放和反缩放是一个很好的答案,完全避免了舍入误差,同时提高了100倍的速度。 - Craig.Feied
这被称为“定点算术”。https://en.wikipedia.org/wiki/Fixed-point_arithmetic 如果您使用基于2的比例因子而不是像您所做的基于10,那么通过位移进行除法和乘法可以获得进一步的计算收益。当然,这种性能增益取决于您的整体算法和性能分析。 - JimbobTheSailor

21

在进行任何财务计算时,始终使用十进制数,否则您将永远追逐1分钱的舍入误差。


8
除非你有证据表明问题出在数学包上,否则不要浪费时间来优化性能。 - S.Lott
8
这不是对原帖问题的回答。 - Jim Balter

14
  1. 没错,软件算法确实比硬件慢100倍,或者至少要慢得多,大约相差一个数量级的因子为100左右。在以前不敢假设每个80386都有一个80387浮点协处理器的年代,二进制浮点数也需要进行软件模拟,速度会很慢。
  2. 如果你认为纯粹的二进制浮点数可以精确表示所有十进制数,那么你是生活在幻想世界中。二进制数可以组合成二分之一、四分之一、八分之一等形式,但是因为精确的十进制0.01需要两个五分之一和一个四分之一(1/100 = (1/4)*(1/5)*(1/5)),而一个五分之一在二进制中没有精确表示,所以不能完全用二进制值来表示所有十进制值(因为0.01是无法精确表示的反例,但是代表着一大类无法精确表示的十进制数)。

因此,在调用ToString()之前,你必须决定是否能够处理舍入,或者是否需要找到其他机制来处理将结果转换为字符串时的舍入。或者,您可以继续使用十进制算术,因为它将保持准确性,并且一旦支持新的IEEE 754十进制算术的机器发布,速度也会变快。

必要的交叉引用:计算机科学家应该了解的有关浮点算术的一切。这是众多可能网址之一。

关于十进制算术和新的IEEE 754:2008标准的信息,请参见此Speleotrove网站。


1/100需要一个5的除数以及一个二进制分数 - 更准确地说,需要两个5的因子:1/100 =(1/4)(1/5)(1/5)。 1/4可以在二进制浮点数中精确表示;而1/5因子则不能。 - Jonathan Leffler
@LawrenceDol 当然是这样,也没有人说过不是。Jonathan 所说的确切而言是,它们无法在二进制中被完全表示。希望这些年间你的困惑已经消失了。 - Jim Balter

8

只需要使用一个大数并乘以10的幂次方。在完成后,再除以相同的10的幂次方。


1
你可能会认为十进制类型可以通过数据库服务器以这种方式实现,从而提供竞争性能。 - philwalk

6

在财务计算中应始终使用小数。数字的大小并不重要。

我最简单的解释方式是通过一些C#代码。

double one = 3.05;
double two = 0.05;

System.Console.WriteLine((one + two) == 3.1);

那段代码会打印出False,即使3.1等于3.1...

同样的事情...但是使用十进制:

decimal one = 3.05m;
decimal two = 0.05m;

System.Console.WriteLine((one + two) == 3.1m);

现在将会打印出True

如果您想避免这种问题,我建议您坚持使用十进制。


1
真正令人恼火的是,.NET ToString() 方法似乎无法显示这种差异,即使要求显示多达20个小数位。如果你减去这两个数字,你会发现它们之间大约有4.44E-16的差异...但我在ToString方法输出的字符串“3.10000000000000000000”中没有看到任何4。如果你使用BitConverter.ToString(BitConverter.GetBytes(one + two))和(3.1)相同的东西,那么字节确实是不同的(CD-CC-CC-CC-CC-CC-08-40 vs. CC-CC-CC-CC-CC-CC-08-40),然而double.ToString()并没有显示出来! - Triynko
3
如果使用适当的缩放和明确定义的舍入规则,double 的精度可以与 decimal 一样精确。如果没有精确的舍入规则,decimal 通常会像 double 一样出现问题。例如,如果三个人每人购买一百件价格为“3件5美元”的物品,应该收到多少钱? - supercat
这并没有回答OP所问的任何问题。 - Jim Balter
如果 3.1 无法用 double 表示,那么 one + two 被比较的到底是什么呢?它是 float 吗?double?decimal?以上都不是吗?因为我不明白 lol。 - NotAPro

1

我参考了我之前回答这个问题的答案。

使用一个长整型变量,存储你需要追踪的最小值,并相应地显示数值。


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