双精度浮点数的显示与比较

4

前言

我正在研究一个系统,该系统旨在供不理解浮点算术的人使用。因此,浮点数比较的实现不会暴露给使用该系统的人。目前,浮点数的比较是这样进行的(由于历史原因,不能更改):

// If either number is not finite, do default comparison
if (!IsFinite(num1) || !IsFinite(num2)) {
    output = (num1 == num2);
} else {
    // Get exponents of both numbers to determine epsilon for comparison
    tmp = (OSINT32*)&num1+1;
    exp1 = (((*tmp)>>20)& 0x07ff) - 1023;
    tmp = (OSINT32*)&num2+1;
    exp2 = (((*tmp)>>20)& 0x07ff) - 1023;

    // Check if exponent is the same
    if (exp1 != exp2) {
        output = false;
    } else {
        // Calculate epsilon based on the magic number 47 (presumably calculated experimentally)?
        epsilon = pow(2.0,exp1-47);
        output = (fabs(num2-num1) <= eps);
    }
}

问题的关键是,我们根据数字的指数计算epsilon,以防止用户在接口中进行浮点比较时犯错误。 一个重要提示:这是针对非软件程序员的人,因此当他们执行pow(sqrt(2), 2) == 2时,他们不会感到惊讶。也许这不是最好的想法,但正如我所说,它不能改变。

问题

我们遇到了如何向用户显示数字的问题。过去,他们只是将数字显示为15个有效数字。但这会导致以下类型的问题:

>> SHOW 4.1 MOD 1
>>    0.099999999999999996
>> SHOW (4.1 MOD 1) == 0.1
>>    TRUE

比较结果称其正确是由于epsilon的生成。但是数字的打印会令人困惑,为什么0.099999999999999996等于0.1?我们需要一种方法来显示数字,使它代表与之比较为TRUE所需的最短有效位数。因此,对于0.099999999999999996,这将是0.1,对于0.569999999992724327,这将是0.569999999992725。
这是否可能?

1
如果您不能改变底层逻辑,我无法想到一种不对显示结果进行“矫正”的方法来完成此操作。 - Colin Basnett
说实话,我认为这个努力注定要失败。我认为你正在尝试一些无法完美完成的事情。而且越接近完美,人们期望它是完美的危险就越大,当它不能完美时,它偶尔出现意外的伤害也会更大。如果您有一些非常具体的用例满足了,也许这是有意义的。但是,如果您只是认为可以比世界顶级专家更好地处理浮点数,因为所有怪癖都可以轻松解决,... - David Schwartz
@DavidSchwartz 我并不是在尝试超越浮点数。问题非常明确,目标是隐藏浮点比较错误,使那些不是软件工程师的用户看不到。我们目前的解决方案是仅截取精度,这个方法已经很好用了。但我正在努力让它变得更好。如果你有任何实际建设性的意见,可以添加一个答案... - Fantastic Mr Fox
3
“浮点数比较错误” - 这是一个基本的误解。如果两个浮点数值不相等,那就是因为它们不相等。假装它们相等并不能解决问题,这只是在隐藏症状,直到它们在别的地方出现。欢迎来到打鼹鼠游戏!下一个问题:A等于B,B等于C,但A不等于C! - Pete Becker
@MSalters 那我应该更深入地了解这个提案,我误解了整件事情。 - PaperBirdMaster
显示剩余9条评论
4个回答

3
您可以计算(num - pow(2.0, exp - 47))(num + pow(2.0, exp - 47)),将两者转换为字符串,并在范围内搜索最小的小数。
一个double的确切值是mantissa * pow(2.0, exp - 51),其中mantissa是整数值,因此如果您添加/减去pow(2.0, exp - 47),则会将mantissa更改为2^4,这应该是没有舍入误差的(除非在边角情况下,即如果它是二进制<= pow(2,4)>= pow(2, 53) - pow(2,4)。你可能需要检查这些*)。
然后您有两个字符串,请搜索数字不同的第一个位置并在那里截断。虽然有很多舍入情况,特别是当您不仅想要范围内的正确数字,而且还想要最接近输入数字的数字时(但可能不需要)。例如,如果您获得"1.23""1.24",您甚至可能想输出"1.235"。
这也表明您的示例是错误的。0.569999999992724327的epsilon是(到最大精度)0.000000000000003552713678800500929355621337890625。范围为0.5699999999927207738892320776358246803283691406250.569999999992727879316589678637683391571044921875,并将在0.569999999992725处截断(或者如果您更喜欢四舍五入,则为0.569999999992723
一个更容易实现的破解方法是将其输出到最大精度,切掉一个数字,将其转换回double,检查是否正确比较。然后继续削减,直到比较失败。(可以用二分搜索改进)
*它们仍应该是完全可表示的,但是您的比较方法将表现得非常奇怪。考虑num1 == 1num2 == 1 - pow(2.0, -53) = 0.99999999999999988897769753748434595763683319091796875。他们的差异0.00000000000000011102230246251565404236316680908203125低于你的epsilon0.000000000000003552713678800500929355621337890625,但比较会说它们不同,因为它们具有不同的指数

谢谢你的回答,很好。我已经修复了我的示例,感谢你指出了这一点。 - Fantastic Mr Fox
或者,按照第一段所述将这两个数字转换为字符串,并打印它们的公共前缀的长度,再加上1。例如,对于0.99420.9967,应该打印出0.995 - MSalters
@MSalters:这不是一样的吗?共同前缀的长度加一等于它们第一次不同的地方。我曾经实现过它,但一年后才注意到我的实现有时是错误的。虽然我想要一个带有<而不是<=的范围,所以在(1.23999, 124000)的情况下,两个数字都无效,输出必须变成类似于1.239995的东西,这可能会使它更难。 - BeniBela
@BeniBela:这只是一个小变化,这就是为什么我将其作为评论添加的原因。主要的设计问题是要打印什么作为额外的数字。 - MSalters

1
是的,这是可能的。
double a=fmod(4.1,1);
cerr<<std::setprecision(0)<<a<<"\n";
cerr<<std::setprecision(10)<<a<<"\n";
cerr<<std::setprecision(20)<<a<<"\n";

输出:

0.1
0.1
0.099999999999999644729

我认为您只需要确定显示精度水平与您的epsilon值相对应即可。

但 epsilon 值是二进制的,而不是十进制的,因此它不一定对应于 0.1、0.01、0.0001 等十进制障碍。 - Fantastic Mr Fox
是的,使用这种方法可能会得到假阳性结果——尽管如果您的精度足够高,这种情况非常罕见。 - Brent Bradburn
真的,但完全避免这种情况的相应精度是14位有效数字。我们已经想出来了,只是想看看是否可以做得更好,因为像你说的那样,除了罕见的情况外,几乎都会在显示中失去精度。 - Fantastic Mr Fox

0
我们需要一种方法来展示一个数字,它代表与之比较时可以得到 TRUE 的最短有效位数。 难道你不能用粗略的方式做吗?
float num = 0.09999999;
for (int precision = 0; precision < MAX_PRECISION; ++precision) {
    std::stringstream str;
    float tmp = 0;
    str << std::fixed << std::setprecision(precision) << num;
    str >> tmp;
    if (num == tmp) {
        std::cout << std::fixed << std::setprecision(precision) << num;
        break;
    }
}

-1

在您指定的约束条件下,不可能避免混淆用户。首先,0.0999999999999996447 等于 0.1,而 0.1000000000000003664 也等于 0.1,但是 0.0999999999999996447 不等于 0.1000000000000003664。其次,2.00000000000001421 等于 2.0,但是 1.999999999999999778 却不等于 2.0,即使它比 2.00000000000001421 更接近 2.0。

享受。


以下两个人提供了相反的有力论据。我怀疑你需要更好地为此辩护,而不仅仅是给出一些数字例子。 - Fantastic Mr Fox
@Ben:也许最好的方式是显示真实的比较范围。像这样带有Epsilon的数字:0.57 +- 0.000000000000003552713678800500929355621337890625或者0.57 +- 3.5527137E-15。或者通过范围[min,max]来解决指数问题。 - BeniBela
@BeniBela 我喜欢这个想法,但我不认为使用解释器的人应该知道这个比较。更重要的是,他们不会关心它。 - Fantastic Mr Fox
@Ben:你考虑过以完整精度显示数字,但模糊最后几位,使其无法读取吗? - BeniBela
@Ben:这样用户就知道可能还有更多的数字,但它们不重要。下一个想法:白色背景上的白色文本。 - BeniBela
显示剩余2条评论

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