Безопасный отдел операций с плавающей запятой

У меня есть некоторые места в моем коде, где я хочу убедиться, что деление на 2 произвольных числа с плавающей запятой (32-битная одинарная точность) не переполнится. Цель/компилятор не гарантирует (достаточно явно) хорошую обработку -INF/INF и (не полностью гарантирует IEEE 754 для исключительных значений - (возможно, неопределенный) - и цель может измениться). Кроме того, я не могу делать предположения о входных данных для этих нескольких специальных мест, и я привязан к стандартным библиотекам C90.

Я прочитал Что каждый программист должен знать об арифметике с плавающей запятой но, честно говоря, я немного растерялся.

Итак... Я хочу спросить сообщество, поможет ли следующий фрагмент кода, и есть ли способы сделать это лучше/быстрее/точнее/правильнее:

#define SIGN_F(val) ((val >= 0.0f)? 1.0f : -1.0f)

float32_t safedivf(float32_t num, float32_t denum)
{
   const float32_t abs_denum = fabs(denum);
   if((abs_denum < 1.0f) && ((abs_denum * FLT_MAX) <= (float32_t)fabs(num))
       return SIGN_F(denum) * SIGN_F(num) * FLT_MAX;
   else
       return num / denum;
}

Изменить: изменено ((abs_denum * FLT_MAX) < (float32_t)fabs(num)) на ((abs_denum * FLT_MAX) <= (float32_t)fabs(num)) по рекомендации Паскаля Куока.


person Mark A.    schedule 14.08.2014    source источник
comment
Статья Голдберга теряет большинство людей при первом чтении, это ужасный ресурс для новичка, изучающего, как использовать числа f-p и арифметику. Он предназначен для людей, разрабатывающих программное и аппаратное обеспечение для реализации чисел и алгоритмов f-p. Вам было бы лучше начать с en.wikipedia.org/wiki/Floating_point.   -  person High Performance Mark    schedule 14.08.2014
comment
Конечно, лучше попросить прощения, чем разрешения   -  person David Heffernan    schedule 14.08.2014
comment
SIGN_F(val) неправильно извлекает знак -0.0, поэтому 1.0 / -0.0 в конечном итоге будет +FLT_MAX. Вы можете или не можете возражать. Если вы не уверены в IEEE 754, возможно, вы не можете быть уверены и в copysign, но если он есть, вы можете его использовать.   -  person Pascal Cuoq    schedule 14.08.2014
comment
@PascalCuoq Ах, ты прав. Есть ли шанс извлечь знак значения, которое, возможно, равно -0.0f, с помощью набора функций, указанных в C89/90, не пытаясь вмешиваться в битовые шаблоны с плавающей запятой? Поправьте меня, если я ошибаюсь: -0.0 сравнивается точно так же, как 0.0?   -  person Mark A.    schedule 14.08.2014
comment
@МаркА. Да, для ==, > и т. д. знаковые нули неразличимы. Вы должны 1- использовать copysign, или 2- обращаться к представлению через union, или 3- делить на него и вызывать то самое переполнение, которого вы пытаетесь избежать. Это оставляет вам решение 2-.   -  person Pascal Cuoq    schedule 14.08.2014
comment
@PascalCuoq Как я и думал. Я боюсь, что мне будет труднее получить гарантии на битовые шаблоны, чем на исключительное поведение значения. Спасибо.   -  person Mark A.    schedule 14.08.2014
comment
@DavidHeffernan Я не понимаю твоего комментария.   -  person Mark A.    schedule 14.08.2014
comment
Попытка выполнить деление. В случае сбоя определите этот сбой и отреагируйте соответствующим образом. Попытка предсказать неудачу заранее так же сложно, а на самом деле даже сложнее, чем идти вперед и выполнять деление.   -  person David Heffernan    schedule 14.08.2014
comment
@DavidHeffernan А. Спасибо за отзыв. В этом особом случае мы не очень доверяем цели, чтобы поймать ошибку и что у меня есть возможность восстановиться, поскольку спецификации ниже уровня языка C немного в потоке. Я должен предположить, что переполнение приведет к неопределенному поведению, поэтому я должен спросить разрешения. :-)   -  person Mark A.    schedule 14.08.2014
comment
@Pascal Cuoq Хороший atan2f(y,x) различает +0.0 и -0.0 с atan2f(zero, -1.0), который возвращает +pi или -pi в зависимости от +0, -0.   -  person chux - Reinstate Monica    schedule 14.08.2014
comment
вместо определения SIGN_F вы можете использовать signbit или copysign Есть ли стандартная функция знака (signum, sgn) в C/C++?   -  person phuclv    schedule 09.04.2019


Ответы (4)


В ((abs_denum * FLT_MAX) < (float32_t)fabs(num) произведение abs_denum * FLT_MAX может быть округлено до fabs(num). Это не означает, что num / denum математически не больше, чем FLT_MAX, и вам следует опасаться, что это может привести к переполнению, которого вы хотите избежать. Вам лучше заменить это < на <=.


В качестве альтернативного решения, если доступен тип double и он шире, чем float, может оказаться более экономичным вычисление (double)num/(double)denum. Если float является двоичным 32, а double двоичным 64, единственный способ переполнения деления double - это если denum равно (a) нулю (и если denum равно нулю, ваш код также проблематичен).

double dbl_res = (double)num/(double)denum;
float res = dbl_res < -FLT_MAX ? -FLT_MAX : dbl_res > FLT_MAX ? FLT_MAX : (float)dbl_res;
person Pascal Cuoq    schedule 14.08.2014
comment
Не могли бы вы указать, где/если мой (исправленный) код проблематичен в случае 0.0, помимо проблемы со знаком? - person Mark A.; 14.08.2014
comment
@МаркА. Я имел в виду только проблему со знаком. - person Pascal Cuoq; 14.08.2014
comment
Downvoter, какое-нибудь объяснение, почему вы находите этот ответ особенно плохим? - person Pascal Cuoq; 18.08.2014
comment
Я также удивлен, потому что это решение - вместе с каким-то детектором знака -0.0, казалось, было для нас. Я не вижу никаких дополнительных подводных камней, учитывая, что наши поплавки и двойники соблюдают условие. Но это может быть обеспечено препроцессором. В чем причина понижения? - person Mark A.; 19.08.2014
comment
Хм... Кажется, что больше нечего сказать о отрицательном голосовании... если вы также пока не знаете, почему может возникнуть проблема. Я бы пошел за вашим ответом. - person Mark A.; 06.09.2014
comment
@МаркА. Голосование против может исходить от кого-то из школы мысли «результаты с плавающей запятой случайны», кому идея обсудить нюанс между < и <= вместо сравнения «с точностью до эпсилон», что бы это ни значило в данном случае (добавить эпсилон или вычесть его? какое значение эпсилон?), может быть анафемой. - person Pascal Cuoq; 06.09.2014
comment
@МаркА. Почему я могу быть прав, когда предлагаю простое решение, не связанное с эпсилон? Я начал работать над искусством предсказания того, что может сделать программа C, использующая числа с плавающей запятой, в 2009 году (hisseo .saclay.inria.fr/index.html ), и с тех пор я не останавливался. - person Pascal Cuoq; 06.09.2014

Вы можете попытаться извлечь показатели и мантиссы num и denum и убедиться, что условие:

((exp(num) - exp (denum)) > max_exp) &&  (mantissa(num) >= mantissa(denum))

И в соответствии со знаком входных данных сгенерируйте соответствующий INF.

person Garp    schedule 14.08.2014
comment
Не уверен, что это точно правильная формула, но это правильный подход. - person Hot Licks; 14.08.2014
comment
Я думаю, что это правильно, если вы используете округление до ближайшего режима. - person Garp; 14.08.2014
comment
Если exp(num) - exp(denum) значительно больше, чем max_exp, значения мантиссы не имеют значения, и переполнение все равно происходит. Также показатель степени (0.0) обычно равен 0, а затем num == 0 или denum == 0 требуют специальной обработки. - person chux - Reinstate Monica; 14.08.2014

Осторожно работайте с num, denom, когда частное близко к FLT_MAX.

В следующем используются тесты, вдохновленные OP, но они не соответствуют результатам около FLT_MAX. Как указывает @Pascal Cuoq, округление может просто подтолкнуть результат к краю. Вместо этого он использует пороги FLT_MAX/FLT_RADIX и FLT_MAX*FLT_RADIX.

При масштабировании с помощью FLT_RADIX, обычно 2, код всегда должен получать точные результаты. Ожидается, что округление в любом режиме округления не повлияет на результат.

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

static int SD_Sign(float x) {
  if (x > 0.0f)
    return 1;
  if (x < 0.0f)
    return -1;
  if (atan2f(x, -1.0f) > 0.0f)
    return 1;
  return -1;
}

static float SD_Overflow(float num, float denom) {
  return SD_Sign(num) * SD_Sign(denom) * FLT_MAX;
}

float safedivf(float num, float denom) {
  float abs_denom = fabsf(denom);
  // If |quotient| > |num|
  if (abs_denom < 1.0f) {
    float abs_num = fabsf(num);
    // If |num/denom| > FLT_MAX/2 --> quotient is very large or overflows
    // This computation is safe from rounding and overflow.
    if (abs_num > FLT_MAX / FLT_RADIX * abs_denom) {
      // If |num/denom| >= FLT_MAX*2 --> overflow
      // This also catches denom == 0.0
      if (abs_num / FLT_RADIX >= FLT_MAX * abs_denom) {
        return SD_Overflow(num, denom);
      }
      // At this point, quotient must be in or near range FLT_MAX/2 to FLT_MAX*2
      // Scale parameters so quotient is a FLT_RADIX * FLT_RADIX factor smaller.
      if (abs_num > 1.0) {
        abs_num /= FLT_RADIX * FLT_RADIX;
      } else {
        abs_denom *= FLT_RADIX * FLT_RADIX;
      }
      float quotient = abs_num / abs_denom;
      if (quotient > FLT_MAX / (FLT_RADIX * FLT_RADIX)) {
        return SD_Overflow(num, denom);
      }
    }
  }
  return num / denom;
}

SIGN_F() нужно учитывать в denum +0.0 или -0.0. Различные методы, упомянутые @Pascal Cuoq в комментарии:

  1. copysign() or signbit()
  2. Используйте союз

Кроме того, некоторые функции при правильной реализации различаются по +/- нулю, например atan2f(zero, -1.0) и sprintf(buffer, "%+f", zero).

Примечание. Для простоты используется float, а не float32_t.
Примечание. Возможно, вместо fabs() используется fabsf().
Второстепенное значение: предлагается denom (знаменатель) вместо denum.

person Community    schedule 14.08.2014
comment
В Википедии должен быть «список функций C, различающих +0.0 и -0.0». - person Pascal Cuoq; 15.08.2014
comment
Спасибо за этот развернутый ответ. Трюк с atan2 умен. Несколько замечаний от меня: --- Я думаю, что функции math.h xxxf, такие как fabsf, впервые появились в C99. Я уверен в этом насчет atan2f. То же самое верно для copysign и signbit. --- Для решения объединения я выберу это для окончательной оптимизации, когда для окончательных настроек компилятора я смогу получить гарантированный битовый шаблон от нашего поставщика компилятора. --- Для нашего приложения у нас есть строгие гарантии времени выполнения, поэтому мы находимся в среднем случае == наихудшем дизайне, где код становится проще. Так что у нас нет счастливого пути. - person Mark A.; 19.08.2014
comment
@Mark A. Чтобы узнать об альтернативных способах отличить +0,0 от -0,0, см. stackoverflow.com/questions/25332133/. Добавьте туда решения, если найдете другое. - person chux - Reinstate Monica; 19.08.2014

Чтобы избежать угловых случаев с округлением и тому подобным, вы можете массировать экспоненту делителя - с помощью frexp() и ldexp() - и беспокоиться о том, можно ли уменьшить результат без переполнения. Или frexp() оба аргумента, и работа с экспонентой выполняется вручную.

person Community    schedule 16.08.2014