【问题标题】:Is there a C/C++ function to safely handle division by zero?是否有一个 C/C++ 函数可以安全地处理除以零?
【发布时间】:2010-09-17 22:19:03
【问题描述】:

我们有一种情况,我们想要对两个值 w1w2 进行加权平均,基于另外两个值 v1 & v2 远离零...例如:

  • 如果 v1 为零,则根本不会加权,因此我们返回 w2
  • 如果 v2 为零,则根本不会加权,因此我们返回 w1
  • 如果两个值均远离零,我们进行平均并返回 (w1 + w2 )/2

我继承了如下代码:

float calcWeightedAverage(v1,v2,w1,w2)
{
  v1=fabs(v1);
  v2=fabs(v2);
  return (v1/(v1+v2))*w1 + (v2/(v1+v2)*w2);
}

对于一些背景知识,v1 和 v2 表示两个不同的旋钮转动了多远,它们各自产生的效果的权重仅取决于它们转动了 多少,而不是朝哪个方向转动.

很明显,当v1==v2==0 出现问题时,因为我们以return (0/0)*w1 + (0/0)*w2 结尾,而您不能这样做 0/0。对 v1==v2==0 进行特殊测试在数学上听起来很糟糕,即使浮点数的做法也不错。

所以我想知道

  • 有一个标准库函数来处理这个问题
  • 有更简洁的数学表示

【问题讨论】:

  • v1==v2==0 在编程上听起来也很糟糕 - 它不会做一些人可能认为它应该做的事情:-)
  • 这不是代码,而是两者都为零的逻辑条件。很抱歉造成混乱...
  • @John,这就是我的假设 - 只是忍不住发表评论 :-)
  • @John:0.0 的测试实际上在数学上是可辩护的,因为数学函数F(x, y) = x / (x + y)x = 0, y = 0 是不连续的。因此,您必须对这一点进行特殊处理 - 在这种情况下,您将用极限替换它,因为 xy 都接近 0(幸运的是,它是已定义且有限的)。
  • @caf: (x,y) 沿轨迹 (r,r) 接近 (0,0) 时的极限,r 接近 0。这个路径极限存在,但在 (0, 0) 不可移动。

标签: c++ c math floating-point divide-by-zero


【解决方案1】:

你正在尝试实现这个数学函数:

F(x, y) = (W1 * |x| + W2 * |y|) / (|x| + |y|)

这个函数在x = 0, y = 0 点是不连续的。不幸的是,正如 R. 在评论中所说,不连续性是不可移除的 - 在这一点上没有任何合理的价值可供使用。

这是因为“合理值”会根据您到达x = 0, y = 0 的路径而变化。例如,考虑从r = R1r = 0 的路径F(0, r)(这相当于将X 旋钮设置为零,并平滑地将Y 旋钮从R1 向下调整为0)。 F(x, y) 的值将保持在 W2 不变,直到您到达不连续点。

现在考虑遵循F(r, 0)(将 Y 旋钮保持为零并将 X 旋钮平滑地调整到零) - 输出将保持在 W1 不变,直到您到达不连续点。

现在考虑关注F(r, r)(将两个旋钮保持在相同的值,并同时将它们调低至零)。此处的输出将保持在W1 + W2 / 2,直到您到达不连续点。

这意味着W1W2 之间的任何 值与x = 0, y = 0 的输出同样有效。在它们之间没有明智的选择。 (此外,始终选择 0 作为输出是完全错误的 - 否则输出将在区间 W1..W2 上(即,对于您接近不连续性的任何路径,F() 的限制始终在该区间内),而 0 甚至可能不在这个区间内!)


您可以通过稍微调整函数来“修复”该问题 - 在fabs() 之后为v1v2 添加一个常量(例如1.0)。这将使每个旋钮的最小贡献不能为零 - 只是“接近零”(常数定义了接近程度)。

将这个常数定义为“一个非常小的数字”可能很诱人,但这只会导致输出发生剧烈变化,因为旋钮被操纵到接近零点,这可能是不可取的。

【讨论】:

  • 实际上,当任一权重接近零时,这将导致输出变化较小。当没有添加小常数时,输出非常敏感。
  • @Ben Voigt:我的意思是,如果常数是 1e-7 并且 v1 是 0,那么将 v22e-7 更改为 1e-7 将改变 @ 的输出987654347@ 至0.33 * w1 + 0.66 * w2。显然,常数 0 是最糟糕的。
  • +1 用于在编程背景下解释第一年的数学分析。
【解决方案2】:

这是我能很快想到的最好的方法

float calcWeightedAverage(float v1,float v2,float w1,float w2)
{
    float a1 = 0.0;
    float a2 = 0.0;

    if (v1 != 0)
    { 
        a1 = v1/(v1+v2) * w1;
    }

    if (v2 != 0)
    { 
        a2 = v2/(v1+v2) * w2;
    }

    return a1 + a2;
}

【讨论】:

  • 您和 Mike 的解决方案都涉及对浮点数为零的显式相等检查,这是 a) 不可避免 b) 不好吗?或者不管我们有多接近零,渐近线都会抵消吗?
  • @John:如果除数不完全为 0,则除法操作应该成功。对吗?
  • 不,不应该。 145.0 除以 1e-7 等于多少?请记住,它必须适合浮子内部。这将是一个数字溢出。
  • @wheaties:嗯? 145/1e-7 = 1.45e9 这很容易表达为浮点数。然而,三种情况是 BIG/small 会溢出浮点数。但在这种情况下,v1/(v1+v2) 从上方以 1 为界,并且 w1 可以表示为浮点数,所以你很好。
  • 这只是隐藏了问题 - 当v1 == v2 == 0 不正确时使用0 作为输出(如果0 不在w1..w2 范围内,那么它不是沿任何不连续的路径)。
【解决方案3】:

我看不出这样做有什么问题:

float calcWeightedAverage( float v1, float v2, float w1, float w2 ) {
    static const float eps = FLT_MIN; //Or some other suitably small value.
    v1 = fabs( v1 );
    v2 = fabs( v2 );

    if( v1 + v2 < eps )
        return (w1+w2)/2.0f;
    else
        return (v1/(v1+v2))*w1 + (v2/(v1+v2)*w2);
}

当然,没有什么“花哨”的东西可以弄清楚你的部门,但为什么要让它变得更难呢?

【讨论】:

    【解决方案4】:

    我个人认为明确检查除以零有什么问题。我们都这样做,所以可以说没有它更丑。

    但是,可以关闭 IEEE 除零异常。你如何做到这一点取决于你的平台。我知道在 Windows 上它必须在整个进程范围内完成,所以如果你不小心的话,你可能会无意中弄乱其他线程(以及它们与你)。

    但是,如果您这样做,您的结果值将是 NaN,而不是 0。我非常怀疑这就是你想要的。如果您在得到 NaN 时无论如何都必须使用不同的逻辑在其中进行特殊检查,那么您最好只在前面检查分母中的 0。

    【讨论】:

      【解决方案5】:

      因此,对于加权平均值,您需要查看两者都为零的特殊情况。在这种情况下,您希望将其视为 0.5 * w1 + 0.5 * w2,对吗?这个怎么样?

      float calcWeightedAverage(float v1,float v2,float w1,float w2)
      {
        v1=fabs(v1);
        v2=fabs(v2);
        if (v1 == v2) {
          v1 = 0.5;
        } else {
          v1 = v1 / (v1 + v2); // v1 is between 0 and 1
        }
        v2 = 1 - v1; // avoid addition and division because they should add to 1      
      
        return v1 * w1 + v2 * w2;
      }
      

      【讨论】:

        【解决方案6】:

        您应该测试fabs(v1)+fabs(v2)==0(这似乎是最快的,因为您已经计算了它们),并返回在这种情况下有意义的任何值(w1+w2/2?)。否则,保持代码不变。

        但是,如果v1==v2==0 是可能的,我怀疑算法本身会被破坏。当旋钮“接近 0”​​时,这种数值不稳定性似乎是不可取的。

        如果行为实际上是正确的并且您想避免特殊情况,您可以在获取绝对值后将给定类型的最小正浮点值添加到v1v2。 (请注意,DBL_MIN 和朋友不是正确的值,因为它们是最小 标准化 值;您需要所有正值中的最小值,包括次正态值。)除非它们'已经非常小了;在通常情况下,添加只会产生v1v2

        【讨论】:

        • -0.5(向上取整) 所以:fabs(v1) + fabs(v2) == 0 测试 v1v2 是否同样远离零?我可能错了……英语是我的第二(实际上是第三)语言……但是这里的某个地方有些可疑
        • 它测试它们是否都是0,只有一个条件。如果其中任何一个不为零,则现有代码“工作”(在一种奇怪的工作意义上......)。
        • 这并不奇怪。如果 v1 和 v2 离零的距离相等,那么它们的绝对值相同并且 (v1/(v1+v2)) == (v2/(v1+v2)) == 1/2
        • 阅读 cmets 关于 OP 下面函数的不连续性。这就是我所说的奇怪。
        【解决方案7】:

        使用显式检查为零的问题在于,除非您按照 cafs 响应中的概述小心谨慎(如果它在您的算法的核心,则 if 可能会很昂贵 - 但不要在意关于那个,直到你测量......)

        我倾向于使用一些可以使权重接近零的东西。

        float calcWeightedAverage(v1,v2,w1,w2)
        {
          eps = 1e-7; // Or whatever you like...
          v1=fabs(v1)+eps;
          v2=fabs(v2)+eps;
          return (v1/(v1+v2))*w1 + (v2/(v1+v2)*w2);
        }
        

        您的函数现在是平滑的,没有渐近线或被零除,只要 v1 或 v2 之一显着高于 1e-7,它就无法与“真实”加权平均值区分开来。

        【讨论】:

          【解决方案8】:

          如果分母为零,你希望它如何默认?你可以这样做:

          static inline float divide_default(float numerator, float denominator, float default) {
              return (denominator == 0) ? default : (numerator / denominator);
          }
          
          float calcWeightedAverage(v1, v2, w1, w2)
          {
            v1 = fabs(v1);
            v2 = fabs(v2);
            return w1 * divide_default(v1, v1 + v2, 0.0) + w2 * divide_default(v2, v1 + v2, 0.0);
          }
          

          注意,静态内联的函数定义和使用应该真正让编译器知道它可以内联。

          【讨论】:

          • 默认是平均平均值 (w1 + w2 )/2,因为 v1v2 与 0 的距离相等
          【解决方案9】:

          这应该可以工作

          #include <float.h>
          
          float calcWeightedAverage(v1,v2,w1,w2)
          {
            v1=fabs(v1);
            v2=fabs(v2);
            return (v1/(v1+v2+FLT_EPSILON))*w1 + (v2/(v1+v2+FLT_EPSILON)*w2);
          }
          

          编辑: 我看到某些精度可能存在问题,因此不要使用 FLT_EPSILON,而是使用 DBL_EPSILON 来获得准确的结果(我猜你会返回一个浮点值)。

          【讨论】:

          • 您知道FLT_EPSILON 是什么,还是因为它的名称中有epsilon 而选择了它?
          【解决方案10】:

          我会这样做:

          float calcWeightedAverage(double v1, double v2, double w1, double w2)
          {
            v1 = fabs(v1);
            v2 = fabs(v2);
            /* if both values are equally far from 0 */
            if (fabs(v1 - v2) < 0.000000001) return (w1 + w2) / 2;
            return (v1*w1 + v2*w2) / (v1 + v2);
          }
          

          【讨论】:

          • 您无法以可移植的方式捕获浮点异常。
          • -1 表示荒谬的 epsilon 滑稽动作,而不是适当的分析。
          • @R. 0.000000001 可能正在推动它;但正确的分析应该是什么?
          猜你喜欢
          • 1970-01-01
          • 2013-03-06
          • 1970-01-01
          • 1970-01-01
          • 2012-11-27
          • 1970-01-01
          • 2011-05-21
          • 1970-01-01
          相关资源
          最近更新 更多