Есть ли функция C / C ++ для безопасной обработки деления на ноль?

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

Так что я подумал, если

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

person Mr. Boy    schedule 17.09.2010    source источник
comment
v1==v2==0 программно тоже звучит ужасно - он не будет делать то, что некоторые могут подумать :-)   -  person Péter Török    schedule 18.09.2010
comment
Это не было задумано как код, а было логическим условием, что оба они равны нулю. Извините за недопонимание...   -  person Mr. Boy    schedule 18.09.2010
comment
@John, это то, что я предполагал - просто не мог удержаться от комментариев :-)   -  person Péter Török    schedule 18.09.2010
comment
@John: Тесты для 0.0 на самом деле математически оправданы, потому что математическая функция F(x, y) = x / (x + y) прерывается на x = 0, y = 0. Таким образом, у вас должен быть особый случай этой точки - в этом случае вы заменяете ее пределом, поскольку x и y оба приближаются к 0 (который, к счастью, определен и конечен).   -  person caf    schedule 18.09.2010
comment
@caf: предел, когда (x, y) приближается к точке (0,0) по траектории (r, r), r приближается к нулю. Этот путевой предел существует, но разрыв в точке (0,0) не устраняется.   -  person R.. GitHub STOP HELPING ICE    schedule 18.09.2010
comment
@R: Ах, да, именно так, нет оправданного значения, которое можно было бы использовать для x = 0, y = 0.   -  person caf    schedule 18.09.2010


Ответы (10)


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

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

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

Это потому, что «разумное значение» меняется в зависимости от пути, который вы выберете, чтобы добраться до 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(). Это сделает так, что минимальный вклад каждой ручки не может быть равен нулю - просто «близок к нулю» (константа определяет, насколько близко).

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

person caf    schedule 18.09.2010
comment
Фактически, это приведет к тому, что выходной сигнал будет меняться меньше, когда любой из весов близок к нулю. Выход невероятно чувствителен, когда не добавляется небольшая константа. - person Ben Voigt; 18.09.2010
comment
@Ben Voigt: Я имею в виду, что если константа 1e-7 и v1 равна 0, то изменение v2 с 2e-7 на 1e-7 изменит вывод с 0.25 * w1 + 0.75 * w2 на 0.33 * w1 + 0.66 * w2. Очевидно, что константа 0 хуже всего. - person caf; 18.09.2010
comment
+1 за попытку объяснить первый год математического анализа в контексте программирования. - person Jens Gustedt; 18.09.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;
}
person abelenky    schedule 17.09.2010
comment
И ваше решение, и решение Майка включают явную проверку на равенство числа с плавающей запятой для нуля, это а) неизбежно б) плохо? Или асимптоты сокращаются независимо от того, насколько близко мы приближаемся к нулю? - person Mr. Boy; 18.09.2010
comment
@John: Если делитель не равен нулю, операция деления должна завершиться успешно. Правильно? - person Zan Lynx; 18.09.2010
comment
нет, не должно. Как 145,0 разделить на 1e-7? Помните, он должен помещаться внутри поплавка. Это будет числовое переполнение. - person wheaties; 18.09.2010
comment
@wheaties: А? 145 / 1e-7 = 1.45e9, что вполне может быть выражено как число с плавающей запятой. Однако три случая, когда BIG / small переполнят поплавок. Но в этом случае v1 / (v1 + v2) ограничено сверху 1, а w1 выражается как число с плавающей запятой, так что все в порядке. - person dmckee --- ex-moderator kitten; 18.09.2010
comment
Это просто скрывает проблему - использование 0 в качестве вывода, когда v1 == v2 == 0 неверно (если 0 не лежит в диапазоне w1..w2, то это не предел функции вдоль any пути к разрыву). - person caf; 18.09.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);
}

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

person Jacob    schedule 17.09.2010

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

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

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

person T.E.D.    schedule 17.09.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;
}
person Jason Goemaat    schedule 17.09.2010

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

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

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

person R.. GitHub STOP HELPING ICE    schedule 18.09.2010
comment
-0,5 (округлено в большую сторону) Итак: fabs(v1) + fabs(v2) == 0 проверяет, одинаково ли далеки v1 и v2 от нуля? Я могу ошибаться ... английский - мой 2-й (на самом деле 3-й) язык ... но где-то здесь есть что-то подозрительное - person pmg; 18.09.2010
comment
Он проверяет, являются ли они оба равными нулю, с одним условием. Если любой из них не равен нулю, существующий код работает (в каком-то странном смысле работает ...). - person R.. GitHub STOP HELPING ICE; 18.09.2010
comment
Это не странно. Если v1 и v2 одинаково далеки от нуля, их абсолютные значения одинаковы и (v1 / (v1 + v2)) == (v2 / (v1 + v2)) == 1/2 - person pmg; 18.09.2010
comment
Прочтите комментарии о прерывании функции под OP. Вот что я имею в виду под странным. - person R.. GitHub STOP HELPING ICE; 18.09.2010

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

person Michael Anderson    schedule 18.09.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);
}

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

person Mike Axiak    schedule 17.09.2010
comment
по умолчанию - среднее значение (w1 + w2) / 2, поскольку v1 и v2 одинаково далеки от 0 - person Mr. Boy; 18.09.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);
}

edit: я видел, что могут быть проблемы с некоторой точностью, поэтому вместо использования FLT_EPSILON используйте DBL_EPSILON для точных результатов (я думаю, вы вернете значение с плавающей запятой).

person George    schedule 17.09.2010
comment
Вы знаете, что такое FLT_EPSILON, или вы просто выбрали его, потому что в его названии есть эпсилон? - person R.. GitHub STOP HELPING ICE; 18.09.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);
}
person pmg    schedule 17.09.2010
comment
Вы не можете переносить исключения с плавающей запятой. - person Puppy; 18.09.2010
comment
-1 за нелепые эпсилонские выходки вместо правильного анализа. - person R.. GitHub STOP HELPING ICE; 18.09.2010
comment
@Р. 0,000000001 может подтолкнуть его; но каков был бы правильный анализ? - person pmg; 18.09.2010