何时应该使用double而不是decimal?

297
我可以列举三个使用double(或float)而非decimal的优势:
  1. 占用更少内存。
  2. 速度更快,因为处理器原生支持浮点数运算。
  3. 可以表示更大范围的数字。
但这些优势似乎仅适用于计算密集型操作,例如建模软件中使用。当需要精度时(例如金融计算),当然不应使用double。那么在“正常”应用程序中有没有选择double(或float)而非decimal的实际理由呢?
编辑后补充: 感谢您所有出色的回复,我从中学到了很多。
进一步的问题是:有些人认为double可以更准确地表示实数。声明时,我认为它们通常也更准确地表示它们。但是,当执行浮点数运算时,准确性是否会降低(有时显着降低)?

在.NET中,我如何在decimal和double之间进行选择? - Ian Ringrose
5
这篇文章经常被点赞,但我还是有困惑。比如说,我正在开发一个财务计算应用程序,所以我一直在使用decimal类型。但Math和VisualBasic.Financial函数使用double类型,导致我需要不断地进行类型转换,这让我一直在对decimal的使用感到不确定。 - Jamie Ide
@JamieIde 这太疯狂了,金融函数应该使用 decimal,因为钱总是以小数形式存在。 - Chris Marisic
@ChrisMarisic 但是如果Jamie Ide使用double来处理遗留代码,那么他能做什么呢?那么你也应该使用double,否则会导致许多转换引起舍入误差...难怪他提到了VisualBasic pfffhh..... - Elisabeth
可能是.NET中decimal、float和double的区别是什么?的重复问题。 - Michael Freidgeim
12个回答

330

我认为你已经很好地总结了优点。然而,你遗漏了一点。 decimal 类型仅在表示 十进制 数字时更准确(例如货币/财务计算中使用的数字)。一般来说,double 类型将提供至少与任意实数相同的精度(如果我错了,请纠正我),并且通常速度更快。简单的结论是:在考虑使用哪种类型时,除非需要 decimal 提供的 十进制 精度,否则始终使用 double

编辑:

关于你额外提出的有关浮点数在操作后精度下降的问题,这是一个稍微微妙的问题。确实,在每次操作后,精度(在此处我可互换使用术语)会逐渐降低。这是由于两个原因:

  1. 某些数字(最明显是小数)无法以浮点形式真正表示
  2. 会发生舍入误差,就像你手动进行计算一样。这在很大程度上取决于环境(你要执行多少操作),是否需要对这些误差进行深思熟虑。

在所有情况下,如果你想比较两个理论上应该是等价的浮点数(但是使用不同的计算方法得出),则需要允许一定程度的容忍度(具体数值有所不同,但通常非常小)。

如果想更详细地了解误差准确性产生的特定情况,请参阅维基百科文章中的准确性部分。最后,如果您想深入了解浮点数/操作在机器级别上的严肃和数学讨论,请尝试阅读备受引用的文章计算机科学家应该了解的浮点运算知识


1
你能提供一个在转换为二进制时会丢失精度的十进制数的例子吗? - Mark Cidade
@Mark:1.000001是一个例子,至少根据Jon Skeet的说法。(参见此页面的第3个问题:http://www.yoda.arachsys.com/csharp/teasers-answers.html) - Noldorin
26
@Mark:一个非常简单的例子:0.1在二进制中是一个循环小数,所以它不能在“double”数据类型中被精确表示。现代计算机仍然会打印出正确的值,但这只是因为它们“猜测”了结果——并不是因为它实际上被正确地表示了。 - Konrad Rudolph
2
Decimal 类型在尾数中有93位精度,而 double 大约只有52位。虽然我希望微软支持IEEE 80位格式,即使它必须填充到16字节;这将允许比 doubleDecimal 更大的范围,比 Decimal 更快的速度,支持超越运算(例如 sin(x),log(x)等),并且精度虽然不如 Decimal 那么好,但比 double 好得多。 - supercat
@charlotte:如果你仔细阅读我的完整帖子,你就会明白那是怎么回事了。 - Noldorin

66
您似乎非常了解使用浮点数类型的好处。我倾向于在所有情况下设计小数,并依靠分析器告诉我是否在小数上执行操作会导致瓶颈或减速。在这些情况下,我会“向下转换”为双精度或单精度浮点数,但仅在内部执行,并仔细尝试通过限制执行的数学运算中的有效数字的数量来管理精度损失。
通常,如果您的值是瞬态的(不被重复使用),则可以安全地使用浮点类型。浮点类型的真正问题在于以下三种情况。
  1. 您正在聚合浮点值(在这种情况下,精度错误会累加)
  2. 您基于浮点值构建值(例如,在递归算法中)
  3. 您正在使用非常宽的有效数字进行数学计算(例如,123456789.1 * .000000000000000987654321
编辑 根据C#小数的参考文档
引用:

decimal 关键字表示 128 位数据类型。与浮点类型相比, 小数类型具有更高的精度和较小的 范围,因此适用于金融和货币计算。

因此,为了澄清我上面的说法:
引用:

在所有情况下,我倾向于设计小数,并依靠分析器告诉我是否在小数上执行操作会导致瓶颈或减速。

我只曾在十进制有利的行业工作过。如果你在物理或图形引擎上工作,为浮点类型(float或double)设计可能会更有益。

十进制不是无限精确的(在原始数据类型中表示非整数的无限精度是不可能的),但它比double要精确得多:

  • decimal = 28-29个有效数字
  • double = 15-16个有效数字
  • float = 7个有效数字

编辑2

针对Konrad Rudolph的评论,第1项(上述)绝对是正确的。不精确性的聚合确实会产生复合效应。请参见下面的代码示例:

private const float THREE_FIFTHS = 3f / 5f;
private const int ONE_MILLION = 1000000;

public static void Main(string[] args)
{
    Console.WriteLine("Three Fifths: {0}", THREE_FIFTHS.ToString("F10"));
    float asSingle = 0f;
    double asDouble = 0d;
    decimal asDecimal = 0M;

    for (int i = 0; i < ONE_MILLION; i++)
    {
        asSingle += THREE_FIFTHS;
        asDouble += THREE_FIFTHS;
        asDecimal += (decimal) THREE_FIFTHS;
    }
    Console.WriteLine("Six Hundred Thousand: {0:F10}", THREE_FIFTHS * ONE_MILLION);
    Console.WriteLine("Single: {0}", asSingle.ToString("F10"));
    Console.WriteLine("Double: {0}", asDouble.ToString("F10"));
    Console.WriteLine("Decimal: {0}", asDecimal.ToString("F10"));
    Console.ReadLine();
}

这将输出以下内容:

Three Fifths: 0.6000000000
Six Hundred Thousand: 600000.0000000000
Single: 599093.4000000000
Double: 599999.9999886850
Decimal: 600000.0000000000

如您所见,尽管我们从相同的源常量添加,双精度浮点数的结果不够精确(虽然可能会正确四舍五入),而单精度浮点数则远不如此精确,甚至只剩下两位有效数字。


1
第一点是不正确的。精度/取舍误差仅会在转换中出现,而不是在计算中出现。当然,大多数数学运算都是不稳定的,因此会使误差放大。但这是另一个问题,并且适用于所有精度有限的数据类型,因此特别适用于十进制。 - Konrad Rudolph
2
@Konrad Rudolph,看“EDIT 2”中的示例,这证明了我在第1项中试图表达的观点。通常,这个问题不会显现出来,因为正误差与负误差相抵消,并且它们在总体上洗去,但是聚合相同的数字(如我在示例中所做的)突显了这个问题。 - Michael Meadows
很棒的例子。我刚向我的初级开发人员展示了它,孩子们都感到惊讶。 - Machado
现在你能用2/3来做同样的事情吗?你应该学习一下六十进制数系统,它可以完美地处理2/3。 - gnasher729
1
@gnasher729,使用2/3而不是3/5对于不同类型并不完全适用。有趣的是,浮点值产生了Single: 667660.400000000000,而十进制值产生了Decimal: 666666.7000000000。浮点值比正确值少了一千左右。 - jhenninger
1
有趣的是,我在 .NET Framework 4.72 https://dotnetfiddle.net/GNkswj(与您的相同)和 .NET 6 https://dotnetfiddle.net/IkVfwh 上得到了不同的数字。 - Tim Abell

26

对于基于10的值,例如财务计算,建议使用十进制。

但是,对于任意计算的值,双精度浮点数通常更准确。

例如,如果您想计算投资组合中每条线路的权重,请使用double,因为结果将更接近100%。

在以下示例中,doubleResult比decimalResult更接近1:

// Add one third + one third + one third with decimal
decimal decimalValue = 1M / 3M;
decimal decimalResult = decimalValue + decimalValue + decimalValue;
// Add one third + one third + one third with double
double doubleValue = 1D / 3D;
double doubleResult = doubleValue + doubleValue + doubleValue;

再举一个投资组合的例子:

  • 投资组合中每条投资线的市场价值是一种货币价值,最好表示为十进制数。

  • 每条投资线的权重(= 市场价值 / SUM(市场价值))通常最好表示为双精度浮点数。


6

当你不需要精度时,可以使用double或float。例如,在我编写的平台游戏中,我使用float来存储玩家速度。显然,我在这里并不需要超级精度,因为最终会四舍五入为Int以在屏幕上绘制。


3
只有精度是小数的唯一优点,这是正确的。你不应该问何时应该使用浮点数而不是小数,这应该是你首先考虑的。问题是什么时候应该使用小数(答案就在这里...当精度很重要时)。 - Instance Hunter
4
@Daniel Straight,这很有趣,但我持相反观点。我认为因为性能特性而使用精度较低的类型等同于预优化。在你意识到它的好处之前,你可能需要付出许多次这种预优化的代价。 - Michael Meadows
3
@Michael Meadows,我能理解这个论点。不过需要注意的是,对于过早优化的主要抱怨之一是程序员往往不知道什么会变慢。然而,我们毫无疑问地知道,十进制数比双精度数慢。尽管如此,在大多数情况下,性能提升对用户来说也不会明显。当然,在大多数情况下,也不需要那么高的精度。 - Instance Hunter
十进制浮点数实际上比使用相同位数的二进制浮点数要不精确。十进制的优势在于能够精确表示像财务计算中常见的0.01这样的小数。 - dan04
好的,这并不是完全正确的 :) - 在许多游戏中,浮点数可能是不可取的,因为它们不是一致的。请参见这里 - BlueRaja - Danny Pflughoeft

5
在某些会计中,考虑使用整数类型代替或结合使用。例如,假设您操作的规则要求每个计算结果至少保留6位小数,并且最终结果将四舍五入到最接近的一分钱。
100美元的1/6计算结果为16.66666666666666...,因此在工作表中所需的值将为$16.666667。双精度和十进制浮点数都应该可以准确地计算出这个结果,但是我们可以通过将结果作为整数16666667进行传递来避免任何累积误差。每个后续计算可以使用相同的精度并类似地进行传递。继续上面的例子,我计算了该金额(16666667 * .0825 = 1375000)的德克萨斯州销售税。将这两个值相加(它只是一个简短的工作表)1666667 + 1375000 = 18041667。将小数点向后移动,得到18.041667,即$18.04。
虽然这个简短的例子不会使用双精度或十进制浮点数产生累积误差,但很容易展示一些情况,其中仅仅计算双精度或十进制浮点数并进行传递将导致累积的重大误差。如果您操作的规则要求有限数量的小数位,请通过将每个值乘以10^(所需小数位数),然后除以10^(所需小数位数)来获取实际值,以避免任何累积误差。
在不涉及一分钱的情况下(例如自动售货机),根本没有理由使用非整数类型。只需将其视为计算一分钱,而不是一美元。我曾经看到过代码,其中每个计算都仅涉及整个便士,但使用双精度会导致错误!仅使用整数运算可以解决这个问题。因此,我的非传统答案是,在可能的情况下,放弃双精度和十进制浮点数。

4

取决于你需要它来做什么。

由于float和double是二进制数据类型,因此在舍入数字时会出现一些困难和错误,例如double会将0.1舍入为0.100000001490116,double也会将1/3舍入为0.33333334326441。简单地说,并非所有实数都有准确的double类型表示。

幸运的是,C#还支持所谓的十进制浮点算法,其中数字通过十进制数值系统而不是二进制系统表示。因此,十进制浮点算法在存储和处理浮点数时不会失去精度。这使得它非常适合需要高精度计算的场合。


3

如果您需要与其他语言或平台进行二进制交互,则可能需要使用标准化的float或double类型。


2
注意:本帖子基于http://csharpindepth.com/Articles/General/Decimal.aspx中十进制类型能力的信息和我自己对其含义的解释。我将假设Double是正常的IEEE双精度浮点数。
注2:本文中的最小值和最大值均指数字的大小。
“十进制”类型的优点:
  • “十进制”可以精确地表示可写为(足够短的)小数分数的数字,而双精度浮点数则无法做到。这在财务账簿和类似的场合非常重要,因为重要的是结果与进行计算的人所得出的结果完全相同。
  • “十进制”的尾数比“双精度浮点数”大得多。这意味着对于它的标准化范围内的值,“十进制”将比双倍精度有更高的精度。
“十进制”的缺点
  • 它会慢得多(我没有基准测试,但我猜至少比一级慢,可能更多),十进制不会从任何硬件加速中受益,对其进行算术运算将需要相对昂贵的乘除以10的幂次方(比乘除以2的幂次方要昂贵得多)以匹配加法/减法之前的指数,并在乘法/除法后将指数带回范围。
  • 与double相比,十进制会更早溢出。十进制只能表示±296-1的数字。相比之下,double可以表示接近±21024的数字。
  • 与double相比,十进制会更早下溢。十进制中可表示的最小数字是±10-28。相比之下,如果支持subnormal numbers,则double可以表示低至2-149(约为10-45)的值,否则为2-126(约为10-38)。
  • 十进制所占用的内存是double的两倍。

我的观点是,在处理货币和其他需要完全匹配人类计算的情况下,默认使用“decimal”,并且在其他情况下默认选择使用double。


0

十进制具有更宽的字节,双精度浮点数是CPU本地支持的。 十进制是基于10的,因此在计算十进制时会发生十进制到双精度浮点数的转换。

For accounting - decimal
For finance - double
For heavy computation - double

请注意,.NET CLR仅支持Math.Pow(double,double)。不支持Decimal。
.NET Framework 4
[SecuritySafeCritical]
public static extern double Pow(double x, double y);

0
默认情况下,如果科学计数法比小数显示更短,则双精度值将序列化为科学计数法。(例如,.00000003将变为3e-8)十进制值永远不会序列化为科学计数法。在序列化以供外部使用时,这可能是需要考虑的一个因素。

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