何时通过十进制将浮点数转换为双精度会有益?

8

我们现有的应用程序从文件中读取一些浮点数。这些数字由其他应用程序(称为B应用)写入。这个文件的格式很久以前就已经固定了(无法更改)。在这个文件中,所有的浮点数都以二进制表示为浮点数保存(文件中的4个字节)。

在我们的程序中,一旦读取数据,我们将浮点数转换为双精度浮点数,并在所有计算中使用双精度浮点数,因为这些计算非常复杂,我们关注舍入误差的传播。

我们注意到,当我们通过十进制转换浮点数时(参见下面的代码),我们得到的结果比直接转换精度更高。注意:B应用也在内部使用双精度浮点数,并仅将它们作为浮点数写入文件。假设B应用程序将0.012作为浮点数写入文件。如果我们在读取后将其转换为十进制,然后再转换为双精度浮点数,我们将得到完全准确的0.012,如果我们直接转换,我们将得到0.0120000001043081。

这可以通过简单的赋值而不是从文件中读取来重现:

    float readFromFile = 0.012f;
    Console.WriteLine("Read from file: " + readFromFile); 
    //prints 0.012

    double forUse = readFromFile;
    Console.WriteLine("Converted to double directly: " + forUse);
    //prints 0.0120000001043081

    double forUse1 = (double)Convert.ToDecimal(readFromFile);
    Console.WriteLine("Converted to double via decimal: " + forUse1); 
    //prints 0.012

通过十进制转换从浮点数到双精度数是否总是有益的?如果不是,什么情况下是有益的?

编辑:应用程序B可以以两种方式获取保存的值:

  1. 值可能是计算的结果
  2. 值可能是由用户输入的十进制小数(因此在上面的示例中,用户已在编辑框中键入了0.012并将其转换为双精度数,然后保存为浮点数)

如果您不进行任何转换,直接将浮点数写出来会发生什么? - Sam I am says Reinstate Monica
@SamIam:在编写它们时?不会有太大变化。问题在于你对它们进行了大量的数学运算。每个操作都会添加一点舍入误差,直到你的数字距离真实值太远而无法使用。(某些情况需要比其他情况更长的时间才能表现出这种行为。)只是为了清楚起见,双精度浮点数也会出现这种情况;只是需要更长的时间才能使错误变得足够大以扰乱计算。不过,通过比较很容易注意到这一点。 - cHao
1
为什么不直接使用“十进制数”? - Bobson
@SamIam:我添加了一行代码到帖子中来解决这个问题。 - farfareast
3个回答

13

我们得到的确切值为0.012。

不,你没有。无论是 float 还是 double 都不能精确表示3/250。你真正得到的是一个由字符串格式化程序 Double.ToString() 渲染为 "0.012" 的值。但这是因为该格式化程序没有显示精确值。

通过 decimal 进行舍入。很可能更快(更易于理解)地使用想要的舍入参数 Math.Round。如果你关心的是有效数字的数量,请参见:


值得一提的是,0.012f(表示最接近0.012的32位IEEE-754值)确切等于

0x3C449BA6
或者
0.012000000104308128

这个值可以用 System.Decimal 精确表示。但是,Convert.ToDecimal(0.012f) 不会给你那个精确的值 - 根据文档中的说明存在一个舍入步骤

此方法返回的 Decimal 值最多包含七个有效数字。如果值参数包含超过七个有效数字,则使用四舍五入进行舍入。


1
@Jon:decimal 可以表示的值的集合不是 double 可以表示的值的超集。取 eps = double.Epsilon,比较 (decimal)eps(decimal)(2 * eps)(decimal)(3 * eps),然后告诉我 decimal 具有更好的精度。 - Ben Voigt
@Jon:我即将检查真正的.NET,以防它是一个强制转换的Mono bug。 - Ben Voigt
@farfareast:使用decimal没有什么好处。如果你想要Convert.ToDouble(Single)得到的舍入效果,就直接用Math.Round进行四舍五入即可。 - Ben Voigt
1
@BenVoigt:我们正在进行一场有趣的对话:-),但是我不能只使用Math.Round,因为第二个参数_digits_指定了我想要保留小数点后多少位数字,但是我可能希望保留5个有效数字,例如1.2345e-30。另一个问题是,一些来自应用程序B的值最初是由(人类)用户通过用户界面输入的。 - farfareast
@farfareast:计算传递给Math.Round的四舍五入位置仍然比调用Convert.ToDecimal()更快。转换为十进制会计算四舍五入位置,然后还会进行大量的其他工作。 - Ben Voigt
显示剩余23条评论

2
尽管看起来很奇怪,但通过十进制转换(使用 Convert.ToDecimal(float))在某些情况下可能是有益的。 如果已知原始数字是由用户提供的十进制表示,并且用户最多输入了7个有效数字,则它将提高精度。 为了证明这一点,我编写了一个小程序(见下文)。以下是解释:
正如您从 OP 中回想起的那样,这是一系列步骤:
应用程序B有两个来源的双精度数:(a) 计算结果;(b) 从用户键入的十进制数转换而来。
应用程序B将其双精度数写成浮点数并存储到文件中,实际上是将52位二进制数字(IEEE 754单精度)进行二进制舍入处理,变成23位二进制数字(IEEE 754双精度)。
我们的应用程序通过以下两种方式之一读取该浮点数并将其转换为双精度数: (a) 直接赋值给双精度数 - 实际上是用二进制零填充23位数字,使其变成52位数字(29个零位); (b) 通过使用(double)Convert.ToDecimal(float)进行十进制转换。
正如Ben Voigt所指出的那样,Convert.ToDecimal(float)(参见MSDN的Remark部分)会将结果四舍五入到7个有效十进制数字。在维基百科的IEEE 754文章中,我们可以看到精度为24位-相当于log10(pow(2,24))≈7.225个十进制数字。因此,当我们进行转换为十进制时,我们失去了0.225个十进制数字。
因此,在一般情况下,如果没有关于双精度浮点数的其他信息,转换为十进制通常会使我们失去一些精度。
但是!如果有额外的知识表明,最初(在写入文件为浮点数之前),双精度浮点数是不超过7位数字的十进制数,则十进制舍入引入的舍入误差(上面的第3(b)步)将弥补二进制舍入引入的舍入误差(在第2步中)。
在证明语句的一般情况的程序中,我随机生成双精度数,然后将其转换为单精度数,再将其转换回双精度数(a)直接转换,(b)通过十进制转换。然后我测量原始双精度数与双精度数(a)和双精度数(b)之间的距离。如果双精度数(a)比双精度数(b)更接近原始值,则增加直接转换计数器;反之,如果是通过十进制转换更接近原始值,则增加通过十进制转换计数器。我将这个过程循环1百万次,然后打印出直接转换计数器与通过十进制转换计数器的比率。这个比率大约为3.7,即大约有5次中有4次通过十进制转换会破坏数字。
为了证明“当用户输入数字时”的情况,我使用了同一个程序,唯一的改变是将Math.Round(originalDouble, N)应用于双精度浮点数。因为我从Random类中获得originalDoubles,它们都在0和1之间,所以有效数字的数量与小数点后的位数相同。我通过循环将此方法放置在由用户键入的1位有效数字至15位有效数字之间的N上,然后在图表上绘制出来。即(直接转换比通过十进制转换更好的次数)与用户键入的有效数字数量之间的关系如下图所示: The dependency of (how many times direct conversion is better than via decimal) from the number of significant digits typed by user
正如您所看到的,对于键入的1到7个数字,通过Decimal转换总是优于直接转换。确切地说,对于一百万个随机数,只有1或2个无法通过转换为Decimal而得到改进。
以下是进行比较所使用的代码:
private static void CompareWhichIsBetter(int numTypedDigits)
{
    Console.WriteLine("Number of typed digits: " + numTypedDigits);
    Random rnd = new Random(DateTime.Now.Millisecond);
    int countDecimalIsBetter = 0;
    int countDirectIsBetter = 0;
    int countEqual = 0;

    for (int i = 0; i < 1000000; i++)
    {
        double origDouble = rnd.NextDouble();
        //Use the line below for the user-typed-in-numbers case.
        //double origDouble = Math.Round(rnd.NextDouble(), numTypedDigits); 

        float x = (float)origDouble;
        double viaFloatAndDecimal = (double)Convert.ToDecimal(x);
        double viaFloat = x;

        double diff1 = Math.Abs(origDouble - viaFloatAndDecimal);
        double diff2 = Math.Abs(origDouble - viaFloat);

        if (diff1 < diff2)
            countDecimalIsBetter++;
        else if (diff1 > diff2)
            countDirectIsBetter++;
        else
            countEqual++;
    }

    Console.WriteLine("Decimal better: " + countDecimalIsBetter);
    Console.WriteLine("Direct better: " + countDirectIsBetter);
    Console.WriteLine("Equal: " + countEqual);
    Console.WriteLine("Betterness of direct conversion: " + (double)countDirectIsBetter / countDecimalIsBetter);
    Console.WriteLine("Betterness of conv. via decimal: " + (double)countDecimalIsBetter / countDirectIsBetter );
    Console.WriteLine();
}

我并没有建议使用直接转换而不是(double)Convert.ToDecimal(input)。我建议使用Math.Round。在每种情况下,它都像Convert.ToDecimal一样好,但更快(如果您使用合理的算法计算舍入位置)。 - Ben Voigt

1
这里有一个不同的答案 - 我不确定它是否比Ben的更好(几乎肯定不是),但它应该能产生正确的结果:
float readFromFile = 0.012f;
decimal forUse = Convert.ToDecimal(readFromFile.ToString("0.000"));

只要.ToString("0.000")生成“正确”的数字(这应该很容易进行抽查),那么您将得到可用的东西,而不必担心舍入误差。如果您需要更高的精度,只需添加更多的0即可。
当然,如果您实际上需要处理0.012f的最大精度,那么这并没有帮助,但如果是这种情况,那么您首先就不想将其从浮点数转换。

糟糕。感谢您发现了这个问题,@farfareast。 - Bobson
内部使用小数而不是双精度浮点数(如果您是这个意思)不是一个选项,因为有几个原因:1.(最重要的)在我的测试中,小数的算术运算比双精度浮点数慢约15倍,而我们的应用程序需要大量计算;2.许多标准C#数学方法(例如Math.Sqrt)没有采用小数作为参数的重载,因此我们将不得不将小数转换为双精度浮点数,在许多情况下失去精度。 - farfareast
@farfareast - 是的,那就是我想要的。这两个原因都很好,但显然这不是一个选项。使用它们实际上是在以速度换精度,所以它比较慢并不让我感到意外。然而,我真的很惊讶Math.Sqrt没有小数形式。 - Bobson
@farfareast - 出于好奇,我刚刚找到了一个用于decimalSqrt算法,并进行了性能测试。在decimal上,它需要大约4000个时钟周期才能找到平方根,以达到decimal的最大精度;而在double的最大精度下(大约是一半),只需要13个时钟周期就可以找到根。因此,这是一个非常明显的精度与速度之间的权衡,但您不必担心缺少Sqrt - Bobson
@Bobson:我想知道Decimal平方根函数是如何实现的?我认为可以将值放大到一个大整数(可能使用三个UInt64变量表示重叠的部分),使用Double.Sqrt计算近似值,使用“手动实现”的多精度整数运算来细化该近似值以获得准确的整数平方根,然后将结果转换为适当缩放的Decimal。它仍然不会像Double.Sqrt那样快,但我希望它比慢300倍要快得多。 - supercat
显示剩余2条评论

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