为什么浮点数计算在不同的机器上会得出不同的结果?

10

我有一个简单的程序,可以从浮点数值中计算出宽高比。例如,对于值1.77777779,该程序返回字符串"16:9"。我已在我的机器上测试过,它可以正常工作。

该程序如下:

    public string AspectRatioAsString(float f)
    {
        bool carryon = true;
        int index = 0;
        double roundedUpValue = 0;
        while (carryon)
        {
            index++;
            float upper = index * f;

            roundedUpValue = Math.Ceiling(upper);

            if (roundedUpValue - upper <= (double)0.1 || index > 20)
            {
                carryon = false;
            }
        }

        return roundedUpValue + ":" + index;
    }

现在在另一台机器上,我得到完全不同的结果。所以在我的机器上,1.77777779 显示为 "16:9",但在另一台机器上显示为 "38:21"。


阅读<a href="http://docs.sun.com/source/806-3568/ncg_goldberg.html">计算机科学家应该了解的浮点运算</a>所需时间。 - Oddthinking
@Oded - 对不起,我删除了我的评论,我决定将其提升为一个答案。 - ChrisF
1
@Peter:编译器在编译时对常量表达式执行强制类型转换。 - Eric Lippert
@Eric - 谢谢,我就猜到了。 - Peter Lillevold
哎呀,我以前从未见过有关IEEE 754实现差异的后记。我认为那篇文章没有提到这种陷阱。 “不幸的是,IEEE标准不能保证同一程序在所有符合标准的系统上都会产生相同的结果。由于各种原因,大多数程序实际上会在不同的系统上产生不同的结果。” - mbauman
显示剩余2条评论
5个回答

24

以下是来自 C# 规范(第 4.1.6 节)的有趣信息:

浮点运算可能具有比操作结果类型更高的精度。例如,某些硬件架构支持具有更大范围和精度的“扩展”或“长双精度”浮点类型,并隐式地使用此更高精度类型执行所有浮点运算。只有通过极高的性能代价才能使这种硬件架构以较低的精度执行浮点运算。为了不要使实现损失性能和精度,C# 允许使用更高精度的类型进行所有浮点运算。除了提供更精确的结果外,这很少产生任何可衡量的影响。

有可能这就是那个调用 Ceiling 函数所带来的“可衡量的影响”。正如其他人已经指出的那样,对浮点数向上取整会将 0.000000002 的差异变成九个数量级的差异,因为它将 15.99999999 变成 16,将 16.00000001 变成 17。在执行操作前略微差异的两个数字,在操作后差别巨大;这种微小的差异可能由于不同的计算机具有更多或更少的“额外精度”来处理它们的浮点运算。

一些相关问题:

针对你如何从一个浮点数计算宽高比的具体问题,我可能会完全用不同的方式解决。我会创建一个如下的表格:

struct Ratio
{
    public int X { get; private set; }
    public int Y { get; private set; }
    public Ratio (int x, int y) : this()
    {
        this.X = x;
        this.Y = y;
    }
    public double AsDouble() { return (double)X / (double)Y; }
}

Ratio[] commonRatios = { 
   new Ratio(16, 9),
   new Ratio(4, 3), 
   // ... and so on, maybe the few hundred most common ratios here. 
   // since you are pinning results to be less than 20, there cannot possibly
   // be more than a few hundred.
};

现在你的实现为:

public string AspectRatioAsString(double ratio)      
{ 
    var results = from commonRatio in commonRatios
                  select new {
                      Ratio = commonRatio, 
                      Diff = Math.Abs(ratio - commonRatio.AsDouble())};

    var smallestResult = results.Min(x=>x.Diff);

    return String.Format("{0}:{1}", smallestResult.Ratio.X, smallestResult.Ratio.Y);
}

注意现在代码读起来非常像您要执行的操作:从这个常见比率列表中选择一个,使得给定比率与常见比率之间的差异最小。


8
我不会使用浮点数,除非我真的必须使用。由于舍入误差,它们太容易出现这种情况。
你能否将代码更改为双精度工作?(十进制过度了)如果这样做,结果是否更一致?
至于为什么在不同的机器上有所不同,这两台机器之间有什么区别吗?
  • 32位与64位?
  • Windows 7 vs Vista vs XP?
  • Intel vs AMD处理器?(感谢Oded)
这样的某些事情可能是原因。

@Chris:是的,一个是64位机器,而另一个结果不正确的是32位机器。此外,64位机器是Windows 7,32位机器是Windows XP。两者都是英特尔处理器。我会按照其他人建议切换到double并使用math.Round。 - JD.
@JD - 32位与64位可能会解释这个问题,32位精度较低。 - ChrisF
@Chris:真正奇怪的是,在另一个开发者的32位机器上,通过调试(在IDE中)运行例程时,结果是正确的。然而,在IDE之外,结果是不正确的。 - JD.
@JD - 再次说,我会说这是由于舍入误差造成的。过去我也遇到过类似的情况,代码在IDE内和外部的行为不同,在调试和发布模式下也不同。这是因为内存初始化的方式略有不同(如果有的话)。 - ChrisF
1
@ChrisF,在32位上的double类型与64位上的double类型精度并没有差别。它们都使用8字节和相同的存储方案。 - user492238
@user492238 - 这是真的,但我只是提供可能导致问题的建议。如果你有两台机器表现不同,你必须看看有什么不同。 - ChrisF

5
尝试使用Math.Round代替Math.Ceiling。如果你得到了16.0000001并进行四舍五入,你会错误地丢弃这个答案。
其他杂项建议:
  • 双精度浮点数比单精度浮点数更好。
  • (double) 0.1的类型转换是不必要的。
  • 如果无法确定纵横比,则可能需要抛出异常。
  • 如果找到答案后立即返回,可以放弃carryon变量。
  • 也许更准确的检查方法是为每个猜测计算纵横比并将其与输入进行比较。
修改后(未经测试):
public string AspectRatioAsString(double ratio)
{
    for (int height = 1; height <= 20; ++height)
    {
        int    width = (int) Math.Round(height * ratio);
        double guess = (double) width / height;

        if (Math.Abs(guess - ratio) <= 0.01)
        {
            return width + ":" + height;
        }
    }

    throw ArgumentException("Invalid aspect ratio", "ratio");
}

2
当索引为9时,您预期会得到类似于upper = 16.0000001或upper = 15.9999999的结果。您得到的结果将取决于舍入误差,这可能因不同的机器而异。当它是15.999999时,roundedUpValue - upper <= 0.1为真,循环结束。当它是16.0000001时,roundedUpValue - upper <= 0.1为假,循环将继续直到index > 20
相反,也许您应该尝试将upper四舍五入到最接近的整数,并检查其与该整数之间的差的绝对值是否很小。换句话说,使用类似于if (Math.Abs(Math.Round(upper) - upper) <= (double)0.0001 || index > 20)的内容。

谢谢。现在只是测试。我应该更加小心,进行更全面的测试。 - JD.

-1
我们的printf()语句使用浮点值,导致计算机1和计算机2产生不同的舍入方式,尽管两台计算机都包含相同的Visual Studio 2019版本和构建版本。然而发现差异在于稍旧的Windows 10 SDK与最新版本之间。虽然看起来很奇怪,但修复后差异消失了。

这个答案相当主观,我认为更适合作为评论。 - Gnqz

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