Отдел безопасных плавающих точек

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

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

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

#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)), как это рекомендовал Паскаль Куок.

Ответ 1

В ((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 является бинарным32ish и double является двоичным, единственный способ, которым разделение 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;

Ответ 2

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

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

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

Ответ 3

Активно работайте с 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 is +0.0 или -0.0. Различные методы, упомянутые @Pascal Cuoq в комментарии:

  • copysign() или signbit()
  • Использовать объединение

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

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

Ответ 4

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