Существует ли функция C / C ++ для безопасной обработки деления на ноль? - PullRequest
4 голосов
/ 18 сентября 2010

У нас есть ситуация, в которой мы хотим сделать своего рода средневзвешенное значение из двух значений w1 & w2 , основываясь на том, как далеко два других значения 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, а вы не можете do 0/0. Установка специального теста для v1==v2==0 звучит ужасно математически, даже если с числами с плавающей запятой это было неплохо.

Так что мне интересно, если

  • была стандартная библиотечная функция для обработки этого
  • есть более точное математическое представление

Ответы [ 10 ]

14 голосов
/ 18 сентября 2010

Вы пытаетесь реализовать эту математическую функцию:

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

Эта функция прерывиста в точке x = 0, y = 0.К сожалению, как отметил Р. в комментарии, разрыв не является устраняемым - там нет никакого разумного значения для использования в этой точке.

Это потому, что «разумное значение» изменяется в зависимости отпуть, по которому вы доберетесь до x = 0, y = 0.Например, рассмотрите возможность следования по пути F(0, r) от r = R1 до r = 0 (это эквивалентно установке ручки X в ноль и плавной настройке ручки Y с R1 до 0).Значение F(x, y) будет постоянным на W2 до тех пор, пока вы не доберетесь до разрыва.

Теперь рассмотрите возможность следования F(r, 0) (удерживая регулятор Y в нуле и плавно регулируя регулятор X до нуля) -выходной сигнал будет постоянным на уровне W1 до тех пор, пока вы не доберетесь до разрыва.

Теперь рассмотрите возможность следования F(r, r) (удерживая обе ручки на одном и том же значении и одновременно устанавливая их в ноль).Выход здесь будет постоянным на W1 + W2 / 2, пока вы не перейдете к разрыву.

Это означает, что любое значение между W1 и W2 в равной степени действует как выход на x = 0, y = 0.Нет разумного способа выбирать между ними.(И, кроме того, всегда выбирая 0, так как выходной сигнал совершенно неправильный - в противном случае выходной сигнал ограничен интервалом W1..W2 (т. Е. Для любого пути, по которому вы приближаетесь к разрыву, предел F() всегда находится в этом интервале), а 0 может даже не лежать в этом интервале!)


Вы можете «исправить» проблему, слегка изменив функцию - добавив константу (например, 1.0) к v1 иv2 после fabs().Это сделает так, чтобы минимальный вклад каждой ручки не мог быть нулевым - просто «близко к нулю» (константа определяет, насколько близко).

Может быть заманчиво определить эту константу как «оченьsmall number ", но это просто приведет к резкому изменению выходного сигнала, поскольку регуляторами манипулируют близко к их нулевым точкам, что, вероятно, нежелательно.

4 голосов
/ 18 сентября 2010

Это лучшее, что я мог придумать быстро

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;
}
3 голосов
/ 18 сентября 2010

Лично я не вижу ничего плохого в явной проверке деления на ноль.Мы все делаем их, поэтому можно утверждать, что без него более уродливо.

Однако, возможно отключить деление IEEE на ноль исключений .Как вы это сделаете, зависит от вашей платформы.Я знаю, что в Windows это должно быть сделано в рамках всего процесса, поэтому вы можете непреднамеренно связываться с другими потоками (и они с вами), если вы не будете осторожны.

Однако, , еслисделайте так, чтобы ваше значение результата было NaN, а не 0 .Я очень сомневаюсь, что это то, что вы хотите.Если вам все равно придется поставить специальную проверку там с другой логикой, когда вы получите NaN, вы также можете просто проверить на 0 в знаменателе впереди.

3 голосов
/ 18 сентября 2010

Я не вижу, что было бы неправильно, если бы просто делал это:

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);
}

Конечно, нет никаких "причудливых" вещей, чтобы выяснить ваше подразделение, но зачем делать это сложнее, чем должно быть?

1 голос
/ 18 сентября 2010

Проблема с использованием явной проверки на ноль состоит в том, что вы можете столкнуться с разрывом в поведении, если вы не будете осторожны, как указано в ответе кафе (и если это в основе вашего алгоритма, то 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, он будет неотличим от "реального" средневзвешенного значения.

1 голос
/ 18 сентября 2010

Вы должны проверить на fabs(v1)+fabs(v2)==0 (это кажется самым быстрым, учитывая, что вы уже вычислили их), и вернуть любое значение, которое имеет смысл в этом случае (w1+w2/2?).В противном случае сохраните код как есть.

Однако я подозреваю, что сам алгоритм не работает, если возможно v1==v2==0.Такой вид числовой нестабильности, когда ручки «около 0» вряд ли кажется желательным.

Если поведение действительно правильное и вы хотите избежать особых случаев, вы можете добавить минимальное положительное значение с плавающей запятойвведите v1 и v2 после получения их абсолютных значений.(Обратите внимание, что DBL_MIN и друзья не являются правильными значениями, поскольку они являются минимальными нормализованными значениями; вам нужен минимум всех положительных значений, включая субнормальные.) Это не будет иметь никакого эффекта, если ониуже очень маленький;дополнения просто дадут v1 и v2 в обычном случае.

1 голос
/ 18 сентября 2010

Таким образом, при средневзвешенном значении вам нужно , чтобы рассмотреть особый случай, когда оба равны нулю. В этом случае вы хотите рассматривать это как 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;
}
0 голосов
/ 18 сентября 2010

Это должно работать

#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 для получения точных результатов (думаю, вы вернете значение с плавающей запятой).

0 голосов
/ 18 сентября 2010

Я бы сделал так:

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);
}
0 голосов
/ 18 сентября 2010

Если знаменатель равен нулю, как вы хотите, чтобы он по умолчанию? Вы можете сделать что-то вроде этого:

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);
}

Обратите внимание, что определение функции и использование статического inline должны действительно дать компилятору понять, что он может быть встроенным.

...