Когда возникает недолив?

Я попадаю в ситуацию, когда вычисление 1.77e-308/10 вызывает исключение потери значимости, а вычисление 1.777e-308/10 — нет. Это странно, потому что:

Потеря значимости происходит, когда истинный результат операции с плавающей запятой меньше по величине (то есть ближе к нулю), чем наименьшее значение, представленное как обычное число с плавающей запятой в целевом типе данных (из Arithmetic Underflow, Википедия)

Другими словами, если мы вычислим x/y, где и x, и y равны double, то потеря значимости должна произойти, если 0 < |x/y| < 2.2251e-308 (наименьшее положительное нормализованное double равно 2.2251e-308). Таким образом, теоретически и 1.77e-308/10, и 1.777e-308/10 должны вызывать исключение потери значимости. Теория противоречит тому, что я проверил с помощью программы C ниже.

#include <stdio.h>
#include <fenv.h>
#include <math.h>


int main(){
  double x,y;

  // x = 1.77e-308 => underflow
  // x = 1.777e-308 gives  ==> no underflow
  x=1.77e-308;

  feclearexcept(FE_ALL_EXCEPT);
  y=x/10.0;
  if (fetestexcept(FE_UNDERFLOW)) {
    puts("Underflow\n");
  }
  else puts("No underflow\n");
}

Для компиляции программы я использовал gcc program.c -lm; Я также попробовал Clang, который дал мне тот же результат. Любое объяснение?

[Редактировать] Я поделился приведенным выше кодом через эту онлайн-среду разработки.


person zell    schedule 16.02.2017    source источник
comment
Можете ли вы показать значение y?   -  person Adam    schedule 16.02.2017
comment
Как вы определили наименьший нормализованный двойник на вашей машине?   -  person Brick    schedule 16.02.2017
comment
На моей платформе все наоборот: 1.77e-308 вызывает недополнение, а 1.777e-308;` - нет. g++ (Debian 4.9.2-10) 4.9.2   -  person LPs    schedule 16.02.2017
comment
@Brick Я определил наименьшее нормализованное значение double с помощью std::numeric_limits‹double›::min() (с помощью отдельной программы на C++).   -  person zell    schedule 16.02.2017
comment
@ Адам Я только что проверил значение 'y'. Это было, как и ожидалось, а именно y = 1,77e-309 или y = 1,777e-309, если x = 1,77e-308 или x = 1,777e-308 соответственно. Я использовал %g printf для печати значения 'y'.   -  person zell    schedule 16.02.2017
comment
@LP Спасибо. Можете ли вы попробовать gcc или clang на своей платформе?   -  person zell    schedule 16.02.2017
comment
Просто для удовольствия, может быть, проверить min на C, а не на C++? en.cppreference.com/w/c/types/limits   -  person Brick    schedule 16.02.2017
comment
@Кирпич. Спасибо. Только что попробовал. Это дает 2.2251e-308. Я использовал printf (%.5g, DBL_MIN).   -  person zell    schedule 16.02.2017
comment
@zell Я также тестировал это с gcc: все то же самое на моей платформе. А DBL_MIN это 2.2251e-308.   -  person LPs    schedule 16.02.2017
comment
Текст в вопросе и комментарии в коде не соответствуют тому, что дает недополнение, а что нет. Я подозреваю, что комментарии к коду верны, и это будет соответствовать тому, что сообщает @LPs.   -  person Brick    schedule 16.02.2017
comment
@Кирпич. Ты прав. Я редактировал текст. Я также поделился кодом через онлайн-IDE, чтобы вы могли попробовать.   -  person zell    schedule 16.02.2017


Ответы (2)


Потеря значимости — это вопрос не только диапазона, но и точности/округления.

7.12.1 Обработка условий ошибки
Результат теряет значимость, если величина математического результата настолько мала, что математический результат не может быть представлен без экстраординарной ошибки округления в объекте указанного типа. . С11 §7.12.1 6

1.777e-308, преобразованный в ближайший binary64 0x1.98e566222bcfcp-1023, имеет значение (0x198E566222BCFC, 7193376082541820), кратное 10. Таким образом, деление на 10 является точным. Нет ошибки округления.

Я считаю, что это легче продемонстрировать с шестнадцатеричной нотацией. Обратите внимание, что деление на 2 всегда точно, за исключением наименьшего значения.

#include <float.h>
#include <stdio.h>
#include <fenv.h>
#include <math.h>

int uf_test(double x, double denominator){
  printf("%.17e %24a ", x, x);
  feclearexcept(FE_ALL_EXCEPT);
  double y=x/denominator;
  int uf = !!fetestexcept(FE_UNDERFLOW);
  printf("%-24a %s\n", y, uf ? "Underflow" : "");
  return uf;
}

int main(void) {
  uf_test(DBL_MIN, 2.0);
  uf_test(1.777e-308, 2.0);
  uf_test(1.77e-308, 2.0);
  uf_test(DBL_TRUE_MIN, 2.0);

  uf_test(pow(2.0, -1000), 10.0);
  uf_test(DBL_MIN, 10.0);
  uf_test(1.777e-308, 10.0);
  uf_test(1.77e-308, 10.0);
  uf_test(DBL_TRUE_MIN, 10.0);
  return 0;
}

Выход

2.22507385850720138e-308                0x1p-1022 0x1p-1023                
1.77700000000000015e-308  0x1.98e566222bcfcp-1023 0x1.98e566222bcfcp-1024  
1.77000000000000003e-308  0x1.97490d21e478cp-1023 0x1.97490d21e478cp-1024  
4.94065645841246544e-324                0x1p-1074 0x0p+0                   Underflow

// No underflow as inexact result is not too small
9.33263618503218879e-302                0x1p-1000 0x1.999999999999ap-1004  
// Underflow as result is too small and inexact
2.22507385850720138e-308                0x1p-1022 0x1.99999999999ap-1026   Underflow
// No underflow as result is exact
1.77700000000000015e-308  0x1.98e566222bcfcp-1023 0x1.471deb4e8973p-1026   
1.77000000000000003e-308  0x1.97490d21e478cp-1023 0x1.45d40a818394p-1026   Underflow
4.94065645841246544e-324                0x1p-1074 0x0p+0                   Underflow
person chux - Reinstate Monica    schedule 16.02.2017
comment
Отличное объяснение! Спасибо. - person zell; 16.02.2017
comment
Может быть, я упускаю что-то очевидное, но почему двойное отрицание перед fetestexcept()? - person ad absurdum; 16.02.2017
comment
@DavidBowling fetestexcept() возвращает int: значение побитового ИЛИ. Из uf_test() я хотел вернуть только 0 или 1, поэтому использовал !!, но затем не использовал этот результат в этом опубликованном коде. Лучший код использовал бы bool uf = fetestexcept(FE_UNDERFLOW); для достижения той же цели. - person chux - Reinstate Monica; 16.02.2017

Проверка документации на функцию, которую вы вызвали, приводит к определению:

FE_UNDERFLOW результат более ранней операции с плавающей запятой был ненормальным с потерей точности

http://en.cppreference.com/w/c/numeric/fenv/FE_exceptions

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

person Brick    schedule 16.02.2017