67

运行与.NET 中的双乘法是否损坏有关的快速实验并阅读了几篇关于 C# 字符串格式的文章,我认为:

{
    double i = 10 * 0.69;
    Console.WriteLine(i);
    Console.WriteLine(String.Format("  {0:F20}", i));
    Console.WriteLine(String.Format("+ {0:F20}", 6.9 - i));
    Console.WriteLine(String.Format("= {0:F20}", 6.9));
}

将是此 C 代码的 C# 等效项:

{
    double i = 10 * 0.69;

    printf ( "%f\n", i );
    printf ( "  %.20f\n", i );
    printf ( "+ %.20f\n", 6.9 - i );
    printf ( "= %.20f\n", 6.9 );
}

但是 C# 产生输出:

6.9
  6.90000000000000000000
+ 0.00000000000000088818
= 6.90000000000000000000

尽管我在调试器中显示的值等于 6.89999999999999946709(而不是 6.9)。

与显示格式要求的精度的 C 相比:

6.900000                          
  6.89999999999999946709          
+ 0.00000000000000088818          
= 6.90000000000000035527          

这是怎么回事?

(Microsoft .NET Framework 版本 3.51 SP1 / Visual Studio C# 2008 速成版)


我有数值计算的背景和在各种平台上实施区间算术的经验 - 一种由于复杂数值系统的精度限制而估计误差的技术。要获得赏金,请不要尝试解释存储精度 - 在这种情况下,它是 64 位双精度 ULP 的差异。

为了获得赏金,我想知道 .Net 如何(或是否)可以将 double 格式化为 C 代码中可见的请求精度。

4

10 回答 10

69

问题是 .NET在应用格式之前double总是将 a 舍入到 15 位有效十进制数字,无论格式要求的精度如何,也不管二进制数的确切十进制值如何。

我猜 Visual Studio 调试器有自己的格式/显示例程,可以直接访问内部二进制数,因此 C# 代码、C 代码和调试器之间存在差异。

没有任何内置功能可以让您访问 a 的确切十进制值double,或者使您能够将 a 格式化double为特定的小数位数,但是您可以通过拆分内部二进制数并将其重建为自己来做到这一点十进制值的字符串表示形式。

或者,您可以使用 Jon Skeet 的DoubleConverter课程(链接自他的“Binary floating point and .NET”文章)。这有一个ToExactString方法可以返回 a 的精确十进制值double。您可以轻松修改它以将输出舍入到特定精度。

double i = 10 * 0.69;
Console.WriteLine(DoubleConverter.ToExactString(i));
Console.WriteLine(DoubleConverter.ToExactString(6.9 - i));
Console.WriteLine(DoubleConverter.ToExactString(6.9));

// 6.89999999999999946709294817992486059665679931640625
// 0.00000000000000088817841970012523233890533447265625
// 6.9000000000000003552713678800500929355621337890625
于 2009-11-01T21:36:34.807 回答
25
Digits after decimal point
// just two decimal places
String.Format("{0:0.00}", 123.4567);      // "123.46"
String.Format("{0:0.00}", 123.4);         // "123.40"
String.Format("{0:0.00}", 123.0);         // "123.00"

// max. two decimal places
String.Format("{0:0.##}", 123.4567);      // "123.46"
String.Format("{0:0.##}", 123.4);         // "123.4"
String.Format("{0:0.##}", 123.0);         // "123"
// at least two digits before decimal point
String.Format("{0:00.0}", 123.4567);      // "123.5"
String.Format("{0:00.0}", 23.4567);       // "23.5"
String.Format("{0:00.0}", 3.4567);        // "03.5"
String.Format("{0:00.0}", -3.4567);       // "-03.5"

Thousands separator
String.Format("{0:0,0.0}", 12345.67);     // "12,345.7"
String.Format("{0:0,0}", 12345.67);       // "12,346"

Zero
Following code shows how can be formatted a zero (of double type).

String.Format("{0:0.0}", 0.0);            // "0.0"
String.Format("{0:0.#}", 0.0);            // "0"
String.Format("{0:#.0}", 0.0);            // ".0"
String.Format("{0:#.#}", 0.0);            // ""

Align numbers with spaces
String.Format("{0,10:0.0}", 123.4567);    // "     123.5"
String.Format("{0,-10:0.0}", 123.4567);   // "123.5     "
String.Format("{0,10:0.0}", -123.4567);   // "    -123.5"
String.Format("{0,-10:0.0}", -123.4567);  // "-123.5    "

Custom formatting for negative numbers and zero
String.Format("{0:0.00;minus 0.00;zero}", 123.4567);   // "123.46"
String.Format("{0:0.00;minus 0.00;zero}", -123.4567);  // "minus 123.46"
String.Format("{0:0.00;minus 0.00;zero}", 0.0);        // "zero"

Some funny examples
String.Format("{0:my number is 0.0}", 12.3);   // "my number is 12.3"
String.Format("{0:0aaa.bbb0}", 12.3);
于 2010-12-31T15:16:46.507 回答
10

看看这个MSDN 参考。在注释中,它指出这些数字四舍五入到所要求的小数位数。

如果改为使用“{0:R}”,它将产生所谓的“往返”值,请查看此MSDN 参考以获取更多信息,这是我的代码和输出:

double d = 10 * 0.69;
Console.WriteLine("  {0:R}", d);
Console.WriteLine("+ {0:F20}", 6.9 - d);
Console.WriteLine("= {0:F20}", 6.9);

输出

  6.8999999999999995
+ 0.00000000000000088818
= 6.90000000000000000000
于 2009-09-14T14:23:08.127 回答
8

虽然这个问题同时已经结束,但我相信值得一提的是这种暴行是如何产生的。在某种程度上,您可能会责怪 C# 规范,该规范规定双精度数必须为 15 或 16 位(IEEE-754 的结果)。再往前一点(第 4.1.6 节),它声明允许实现使用更高的精度。请注意:更高,而不是更低。它们甚至被允许偏离 IEEE-754:类型x * y / zwherex * y会产生+/-INF但在除法后会在有效范围内的表达式,不必导致错误。此功能使编译器更容易在可以产生更好性能的架构中使用更高的精度。

但我答应了一个“理由”。这是来自共享源 CLI的引用(您在最近的评论之一中请求了资源),位于clr/src/vm/comnumber.cpp

“为了提供既易于显示又可往返的数字,我们使用 15 位数字解析该数字,然后确定它是否往返于相同的值。如果是,我们将该 NUMBER 转换为字符串,否则我们使用 17 位数字重新解析并显示它。”

换句话说:MS 的 CLI 开发团队决定既可以往返,又可以显示漂亮的值,阅读起来并不那么痛苦。是好是坏?我希望选择加入或退出。

找出任何给定数字的这种往返性的技巧是什么?转换为通用的 NUMBER 结构(其中有单独的字段用于 double 的属性)并返回,然后比较结果是否不同。如果不同,则使用精确值(如中间值6.9 - i),如果相同,则使用“漂亮值”。

正如您在对 Andyp 的评论中已经指出的那样,6.90...00按位等于6.89...9467. 现在您知道为什么0.0...8818要使用它了:它与0.0.

这个15 位数的障碍是硬编码的,只能通过重新编译 CLI、使用 Mono 或致电 Microsoft 并说服他们添加一个选项来打印完整的“精度”(这不是真正的精度,而是由于缺少一个更好的词)。自己计算 52 位精度或使用前面提到的库可能更容易。

编辑:如果您想尝试使用 IEE-754 浮点数,请考虑使用此在线工具,它会向您显示浮点数的所有相关部分。

于 2009-11-02T23:18:11.797 回答
7

利用

Console.WriteLine(String.Format("  {0:G17}", i));

这将为您提供它拥有的所有 17 位数字。默认情况下,Double 值包含 15 位精度的十进制数字,但内部维护最多 17 位数字。{0:R} 并不总是给你 17 位数字,如果数字可以用这种精度表示,它会给你 15 位。

如果数字可以用该精度表示,则返回 15 位数字;如果数字只能以最大精度表示,则返回 17 位数字。您无法做任何事情来使双精度返回更多数字,这就是它的实现方式。如果你不喜欢它自己做一个新的双班......

.NET 的 double 不能存储超过 17 位的数字,因此您在调试器中看不到 6.89999999999999946709,您会看到 6.8999999999999995。请提供一张图片来证明我们错了。

于 2009-10-30T00:28:51.733 回答
2

这个问题的答案很简单,可以在MSDN上找到

请记住,浮点数只能逼近十进制数,并且浮点数的精度决定了该数字逼近十进制数的准确度。默认情况下,Double 值包含15 个十进制数字精度,尽管内部维护最多 17 位数字。

在您的示例中, i 的值为 6.89999999999999946709 ,其第 3 位和第 16 位之间的所有位置的数字为 9(请记住计算数字中的整数部分)。转换为字符串时,框架会将数字四舍五入到第 15 位。

i     = 6.89999999999999 946709
digit =           111111 111122
        1 23456789012345 678901
于 2009-10-30T23:38:42.570 回答
2

我试图重现您的发现,但是当我在调试器中看到“i”时,它显示为“6.8999999999999995”,而不是您在问题中写的“6.89999999999999946709”。您能否提供重现您所见内容的步骤?

要查看调试器向您显示的内容,您可以使用 DoubleConverter,如下面的代码行所示:

Console.WriteLine(TypeDescriptor.GetConverter(i).ConvertTo(i, typeof(string)));

希望这可以帮助!

编辑:我想我比我想象的更累,当然这与格式化往返值相同(如前所述)。

于 2009-11-01T00:16:07.363 回答
1

另一种方法,从方法开始:

double i = (10 * 0.69);
Console.Write(ToStringFull(i));       // Output 6.89999999999999946709294817
Console.Write(ToStringFull(-6.9)      // Output -6.90000000000000035527136788
Console.Write(ToStringFull(i - 6.9)); // Output -0.00000000000000088817841970012523233890533

一个插入功能...

public static string ToStringFull(double value)
{
    if (value == 0.0) return "0.0";
    if (double.IsNaN(value)) return "NaN";
    if (double.IsNegativeInfinity(value)) return "-Inf";
    if (double.IsPositiveInfinity(value)) return "+Inf";

    long bits = BitConverter.DoubleToInt64Bits(value);
    BigInteger mantissa = (bits & 0xfffffffffffffL) | 0x10000000000000L;
    int exp = (int)((bits >> 52) & 0x7ffL) - 1023;
    string sign = (value < 0) ? "-" : "";

    if (54 > exp)
    {
        double offset = (exp / 3.321928094887362358); //...or =Math.Log10(Math.Abs(value))
        BigInteger temp = mantissa * BigInteger.Pow(10, 26 - (int)offset) >> (52 - exp);
        string numberText = temp.ToString();
        int digitsNeeded = (int)((numberText[0] - '5') / 10.0 - offset);
        if (exp < 0)
            return sign + "0." + new string('0', digitsNeeded) + numberText;
        else
            return sign + numberText.Insert(1 - digitsNeeded, ".");
    }
    return sign + (mantissa >> (52 - exp)).ToString();
}

这个怎么运作

为了解决这个问题,我使用了 BigInteger 工具。大值很简单,因为它们只需要将尾数左移指数即可。对于较小的值,我们不能直接右移,因为这会丢失精度位。我们必须首先通过将它乘以 10^n 来给它一些额外的大小,然后进行右移。之后,我们将小数点左移 n 位。更多文字/代码在这里

于 2020-12-07T05:51:00.040 回答
0

答案是肯定的,.NET 中的双重打印被破坏了,它们正在打印尾随的垃圾数字。

您可以在此处阅读如何正确实现它。

我不得不为 IronScheme 做同样的事情。

> (* 10.0 0.69)
6.8999999999999995
> 6.89999999999999946709
6.8999999999999995
> (- 6.9 (* 10.0 0.69))
8.881784197001252e-16
> 6.9
6.9
> (- 6.9 8.881784197001252e-16)
6.8999999999999995

注意:C 和 C# 都有正确的值,只是打印错误。

更新:我仍在寻找导致这一发现的邮件列表对话。

于 2009-11-02T18:04:30.437 回答
0

我找到了这个快速修复。

    double i = 10 * 0.69;
    System.Diagnostics.Debug.WriteLine(i);


    String s = String.Format("{0:F20}", i).Substring(0,20);
    System.Diagnostics.Debug.WriteLine(s + " " +s.Length );
于 2015-05-13T11:49:38.863 回答