防止浮动分隔中的上溢/下溢

时间:2018-10-02 09:35:11

标签: c floating-point

我有两个数字:

FL_64 variable_number;
FL_64 constant_number;

常数始终是相同的,例如:

constant_number=(FL_64)0.0000176019966602325;

变量号已给我,我需要进行除法:

FL_64 result = variable_number/constant_number;

为了确保操作在执行之前不会上溢/下溢,我需要对 variable_number 做哪些检查?

编辑:FL_64只是double的typedef,所以FL_64 = double。

5 个答案:

答案 0 :(得分:4)

溢出测试

假设:

  • C实现使用IEEE-754算法,并具有从最接近到最接近的关系。
  • 除数的大小最大为1,除数不为零。
  • 除数为正。

为简单起见,下面的测试和证明是根据上述假设编写的,但一般情况很容易处理:

  • 如果除数可能为负,则在计算以下所示的fabs(divisor)时,请使用divisor代替limit
  • 如果除数为零,则无需测试溢出,因为已经知道会发生错误(被零除)。
  • 如果幅度超过1,则除法永远不会创建新的溢出。仅当股息已经是无穷大时才会发生溢出(因此测试将为isinf(candidate))。 (如果除数的大小超过1,则除法可能会下溢。此答案未讨论在这种情况下的下溢测试。)

关于符号的说明:使用非代码格式运算符的表达式,例如xy,可以表示精确的数学表达式,而无需浮点舍入。代码格式的表达式,例如x*y表示使用浮点舍入的计算结果。

要检测除以divisor时的溢出,可以使用:

FL_64 limit = DBL_MAX * divisor;
if (-limit <= candidate && candidate <= limit)
    // Overflow will not occur.
else
    // Overflow will occur or candidate or divisor is NaN.

证明:

limit等于DBL_MAX乘以divisor并四舍五入到最接近的可表示值。对于某些错误 e ,恰好是DBL_MAXdivisor•(1+ e ),使得−2 −53 e ≤2 −53 ,根据四舍五入到最接近的属性,再加上divisor无可表示的值乘以{{1 },产生一个低于正常范围的值。 (在亚正常范围内,由于四舍五入引起的相对误差可能大于2 −53 。由于乘积保持在正常范围内,所以不会发生。)

但是,仅当DBL_MAXDBL_MAX的确切数学值恰好介于两个可表示的值之间时,才会出现 e = 2 −53 值,因此要求它具有54个有效位(可表示值的53位有效值的最低位置的½的位是从前导位开始计数的第54 位)。我们知道divisor的有效位数是1fffffffffffff 16 (53位)。将其乘以奇数将产生1fffffffffffff 16 (乘以1),5ffffffffffffd 16 (乘以3)和0x9ffffffffffffffb 16 (乘以5) ,以及与更大的奇数相乘时具有更高有效位的数字。请注意,5ffffffffffffd 16 具有55个有效位。这些都没有正好有54个有效位。当乘以偶数时,乘积的末尾为零,因此有效位数与乘以偶数除以2的最大幂所得的奇数相同。因此,DBL_MAX的乘积没有正好在两个可表示值之间,因此错误 e 永远不会精确地为2 −53 。所以−2 53 < e <2 −53

因此,DBL_MAX = limitDBL_MAX•(1+ e ),其中 e <2 −53 。因此divisor / limitdivisor•(1+ e )。由于此结果小于DBL_MAX的½ULP,因此它不会四舍五入到无穷大,因此它不会溢出。因此,将小于或等于DBL_MAX的任何candidate除以limit不会溢出。

现在,我们将考虑超过divisor的候选人。与上限一样,出于相同的原因, e 不能等于−2 −53 。那么最小的 e 可以是−2 −53 + 2 −105 ,因为limit和{{1 }}最多具有106个有效位,因此从两个可表示值之间的中点起的任何增加必须至少为2 −105 的一部分。然后,如果DBL_MAXdivisor至少是limit < candidate的2 -52 的一部分,因为有效位数为53位。因此candidatelimit•(1−2 −53 +2 −105 )•(1 + 2 −52 )<DBL_MAX。那么divisor / candidate至少是candidate•((1-2−sup> −53 +2 −105 )•(1+ 2 −52 ),即divisor•(1 + 2 −53 +2 −157 )。超过DBL_MAX和指数范围不受限制时下一个可表示的值之间的中点,这是IEEE-754舍入标准的基础。因此,它四舍五入到无穷大,所以会发生溢出。

下溢

除以数量级小于一的数字当然会使数量级大,因此它永远不会下溢到零。但是,IEEE-754对下溢的定义是,在舍入之前或之后(无论是在实现之前还是之后使用),非零结果都是很小的(在低于正常范围内)。当然,将次标准数除以DBL_MAX小于1可能会产生仍在次标准范围内的结果。但是,要做到这一点,必须先发生下溢,才能首先获得次要股利。因此,永远不会通过除以小于1的数来引入下溢。

如果确实希望测试该下溢,则可以通过将候选者与最小法线(或最大子法线)乘以DBL_MAX进行比较,来类似于溢出测试,但我尚未进行过工作通过数值属性。

答案 1 :(得分:2)

假设FL_64类似于double,则可以从float.h

获得名为 DBL_MAX 的最大值。

所以您要确保

DBL_MAX >= variable_number/constant_number

或同样

DBL_MAX * constant_number >= variable_number

在可能类似于

的代码中
if (constant_number > 0.0 && constant_number < 1.0)
{
    if (DBL_MAX * constant_number >= variable_number)
    {
        // wont overflow
    }
    else
    {
        // will overflow
    }
}
else
{
    // add code for other ranges of constant_number
}

但是,请注意浮点计算是不精确的,因此在某些极端情况下,上述代码可能会失败。

答案 2 :(得分:2)

我将尝试回答您提出的问题(而不是尝试回答您没有提出的另一种“如何检测无法避免的上溢或下溢”问题)。

为防止在软件设计过程中划分时出现上溢和下溢的情况,

  • 确定分子的范围,并找到绝对值最大和最小的值

  • 确定除数的范围,并找到最大和最小绝对值的值

  • 确保数据类型的最大可表示值(例如FLT_MAX)除以除数范围的最大绝对值大于分子范围的最大绝对值。 / p>

  • 请确保数据类型的最小可表示值(例如FLT_MIN)乘以除数范围的最小绝对值小于分子范围的最小绝对值。 / p>

请注意,对于每种可能的数据类型,可能需要重复最后几步,直到找到防止下溢和下溢的“最佳”(最小)数据类型(例如,您可能会检查float是否满足要求最后2个步骤,发现没有,然后检查double是否满足最后2个步骤,并发现确实需要。

还可能发现没有数据类型能够防止上溢和下溢,并且您必须限制可用于分子或除数或重新排列公式的值的范围(例如,更改{ 1}}转换为(c*a)/b)或切换到其他表示形式(“双精度双精度”,有理数等)。

也;请注意,这可以确保(对于您范围内的所有值组合)防止上溢和下溢;但如果分子和除数的大小之间存在某种关系,则不能保证将选择最小的数据类型。举一个简单的例子,如果您要进行(c/b)*a之类的操作,其中分子的大小取决于除数的大小,那么您将永远不会得到“除数最小的最大分子”或“除数最小的分子”。 “最大除数”的案例和较小的数据类型(无法处理不存在的案例)可能是合适的。

请注意,您也可以在每个单独的分区之前进行检查。这会在导致代码重复的同时(由于分支/检查)使性能变差(例如,在b = a*a+1; result = b/a;可能导致上溢或下溢的情况下,提供使用double的替代代码);并且在支持的最大类型不够大时无法工作(您最终遇到float问题,无法以确保应该起作用的值起作用的方式解决该问题,因为通常情况下,您唯一要做的就是可以将其视为错误条件。

答案 3 :(得分:1)

我不知道您的FL_64遵循什么标准,但是如果类似IEEE 754,您将需要提防

不是数字

可能有一个特殊的NaN值。在某些实现中,将其与任何内容进行比较的结果是0,所以如果是(variable_number == variable_number) == 0,那就是发生了什么。视实现而定,可能会有宏和函数对此进行检查,例如GNU C Library

Infinity

IEEE 754还支持无穷大(和负无穷大)。例如,这可能是溢出的结果。如果variable_number是无限的,然后将其除以constant_number,结果可能会再次变为无限。与NaN一样,该实现通常会提供宏或函数来对此进行测试,否则,您可以尝试将数字除以某个值,然后看数字是否变小。

溢出

由于将数字除以constant_number会使其更大,因此variable_number可能会溢出(如果已经很大)。检查它是否不大到会发生这种情况。但是根据您的任务,可能已经排除了这么大的可能性。 IEEE 754中的64位浮点数最多达到10 ^ 308。如果您的电话号码溢出,则可能变成无穷大。

答案 4 :(得分:1)

我个人不知道FL_64变量类型,从我想它具有64位表示形式的名称来看,但是它是带符号的还是无符号的?

无论如何,只有在对类型进行签名的情况下,我才会看到潜在的问题,否则商和提醒都可以在相同数量的位上重新表示。

如果已签名,则需要检查结果符号:

FL_64 result = variable_number/constant_number;

if ((variable_number > 0 && constant_number > 0) || (variable_number < 0 && constant_number < 0)) {
    if (result < 0) {
        //OVER/UNDER FLOW
        printf("over/under flow");
    } else {
        //NO OVER/UNDER FLOW
        printf("no over/under flow");
    }
} else {
    if (result < 0) {
        //NO OVER/UNDER FLOW
        printf("no over/under flow");
    } else {
        //OVER/UNDER FLOW
        printf("over/under flow");
    }
}

还应检查其他情况,例如除以0。但是正如您所提到的,constant_number始终是固定的,并且不同于0。

编辑:

好的,因此可能存在另一种使用DBL_MAX值检查溢出的方法。通过将最大可表示数量加倍,可以将其乘以constant_number并计算variable_number的最大值。从下面的代码片段中,您可以看到第一种情况不会引起溢出,而第二种情况却会引起溢出(因为variable_numbertest更大)。实际上,从控制台输出中,您可以看到第一个值result高于第二个值,即使它实际上应该是前一个值的两倍。因此,这种情况是溢出情况。

#include <stdio.h>
#include <float.h>

typedef double FL_64;

int main() {
    FL_64 constant_number = (FL_64)0.0000176019966602325;
    FL_64 test = DBL_MAX * constant_number;
    FL_64 variable_number = test;
    FL_64 result;

    printf("MAX double value:\n%f\n\n", DBL_MAX);

    printf("Variable Number value:\n%f\n\n", variable_number);
    printf(variable_number > test ? "Overflow case\n\n" : "No overflow\n\n");
    result = variable_number / constant_number;
    printf("Result: %f\n\n", variable_number);

    variable_number *= 2;

    printf("Variable Number value:\n%f\n\n", variable_number);
    printf(variable_number > test ? "Overflow case\n\n" : "No overflow\n\n");
    result = variable_number / constant_number;
    printf("Result:\n%f\n\n", variable_number);

    return 0;
}

这是一个特殊的案例解决方案,因为您有一个常数值。但是这种解决方案在一般情况下不起作用。

相关问题