Какие операции и функции на +0.0 и -0.0 дают разные арифметические результаты?

В C, когда поддерживается ±0.0, -0.0 или +0.0, назначенный double, обычно не делает арифметической разницы. Хотя они имеют разные битовые шаблоны, они арифметически сравниваются как равные.

double zp = +0.0;
double zn = -0.0;
printf("0 == memcmp %d\n", 0 == memcmp(&zn, &zp, sizeof zp));// --> 0 == memcmp 0
printf("==          %d\n", zn == zp);                        // --> ==          1

Вдохновляем комментарий @Pascal Cuoq, я ищу еще несколько функций в стандарте C, которые предоставляют арифметически разные результаты.

Примечание. Многие функции, такие как sin(), возвращают +0.0 из f(+0.0) и -0.0 из f(-0.0). Но они не дают различных арифметических результатов. Также 2 результата не должны быть NaN.

Ответ 1

Существует несколько стандартных операций и функций, которые формируют численно разные ответы между f(+0.0) и f(-0.0).

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

#include <math.h>

double inverse(double x) { return 1/x; }

double atan2m1(double y) { return atan2(y, -1.0); }

double sprintf_d(double x) {
  char buf[20];
  // sprintf(buf, "%+f", x);   Changed to e
  sprintf(buf, "%+e", x);
  return buf[0];  // returns `+` or `-`
}

double copysign_1(double x) { return copysign(1.0, x); }

double signbit_d(double x) {
  int sign = signbit(x);  // my compile returns 0 or INT_MIN
  return sign;
}

double pow_m1(double x) { return pow(x, -1.0); }

void zero_test(const char *name, double (*f)(double)) {
  double fzp = (f)(+0.0);
  double fzn = (f)(-0.0);
  int differ = fzp != fzn;
  if (fzp != fzp && fzn != fzn) differ = 0;  // if both NAN
  printf("%-15s  f(+0):%-+15e %s  f(-0):%-+15e\n", 
      name, fzp, differ ? "!=" : "==", fzn);
}

void zero_tests(void) {
  zero_test("1/x",             inverse);
  zero_test("atan2(x,-1)",     atan2m1);
  zero_test("printf(\"%+e\")", sprintf_d);
  zero_test("copysign(x,1)",   copysign_1);
  zero_test("signbit()",       signbit_d);
  zero_test("pow(x,-odd)",     pow_m1);;  // @Pascal Cuoq
  zero_test("tgamma(x)",       tgamma);  // @vinc17 @Pascal Cuoq
}

Output:
1/x              f(+0):+inf             !=  f(-0):-inf           
atan2(x,-1)      f(+0):+3.141593e+00    !=  f(-0):-3.141593e+00  
printf("%+e")    f(+0):+4.300000e+01    !=  f(-0):+4.500000e+01   
copysign(x,1)    f(+0):+1.000000e+00    !=  f(-0):-1.000000e+00  
signbit()        f(+0):+0.000000e+00    !=  f(-0):-2.147484e+09 
pow(x,-odd)      f(+0):+inf             !=  f(-0):-inf           
tgamma(x)        f(+0):+inf             !=  f(-0):+inf  

Примечания:
tgamma(x) появился == на моей машине gcc 4.8.2, но правильно != на других.

rsqrt(), AKA 1/sqrt() - это, возможно, будущая стандартная функция C. Май/может и не работать.

double zero = +0.0; memcpy(&zero, &x, sizeof x) может показать, что x - это другой бит, чем +0.0, но x все еще может быть +0.0. Я думаю, что в некоторых форматах FP есть несколько битовых шаблонов, которые +0.0 и -0.0. TBD.

Это автоответ, предоставленный https://stackoverflow.com/help/self-answer.

Ответ 2

Функция IEEE 754-2008 rsqrt (которая будет в будущем стандарту ISO C) возвращает ± ∞ на ± 0, что довольно удивительно. И tgamma также возвращает ± ∞ на ± 0. С MPFR mpfr_digamma возвращает противоположность ± ∞ на ± 0.

Ответ 3

Я думаю об этом методе, но я не могу проверить до уик-энда, поэтому кто-то может сделать некоторые эксперименты на этом, если он или она, или просто скажите мне, что это ерунда:

  • Создать -0.0f. Должно быть возможным генерировать статично путем назначения крошечной отрицательной константы, которая недопустима для представления float.

  • Назначьте эту константу для летучего двойника и обратно для float.

    Изменяя представление битов 2 раза, я предполагаю, что стандартное битное представление компилятора для -0.0f теперь находится в переменная. Компилятор не может перехитрить меня там, потому что полностью другое значение может быть в переменной volatile между этими двумя копиями.

  • сравнить входные данные с 0.0f. Чтобы определить, есть ли у нас файл 0.0f/-0.0f

  • если он равен, назначьте двойную переменную ввода volitale, а затем вернитесь к float.

    Я снова предполагаю, что теперь он имеет стандартное представление компилятора для 0.0f

  • доступ к битовым шаблонам с помощью объединения и сравнение их, чтобы решить, является ли оно -0.0f

Код может выглядеть примерно так:

typedef union
{
  float fvalue;
  /* assuming int has at least the same number of bits as float */
  unsigned int bitpat;
} tBitAccess;

float my_signf(float x)
{
  /* assuming double has smaller min and 
     other bit representation than float */

  volatile double refitbits;
  tBitAccess tmp;
  unsigned int pat0, patX;

  if (x < 0.0f) return -1.0f;
  if (x > 0.0f) return 1.0f;

  refitbits = (double) (float) -DBL_MIN;
  tmp.fvalue = (float) refitbits;
  pat0 = tmp.bitpat;

  refitbits = (double) x; 
  tmp.fvalue = (float) refitbits;
  patX = tmp.bitpat;

  return (patX == pat0)? -1.0f : 1.0f;

}
  • Это не стандартная функция или оператор, а функция, которая должна различать знаки -0.0 и 0.0.
  • Он основан (главным образом) на предположении, что поставщик компилятора не использует разные шаблоны бит для -0.0f в результате изменения форматов, даже если формат с плавающей запятой позволит это, и если это будет выполнено, оно будет независимым из выбранного битового рисунка.
  • Для форматов с плавающей запятой, которые имеют точный один шаблон для -0.0f, эта функция должна безопасно выполнять трюк без знания порядка бит в этом шаблоне.
  • Другие предположения (о размере типов и т.д.) могут обрабатываться с помощью прекомпиляторов на константах float.h.

Изменить: со второй мыслью: если мы можем заставить значение, сравниваемое с (0.0 || -0.0) ниже наименьшего представляемого денормального (субнормального) числа с плавающей запятой или его отрицательного аналога, и нет второго шаблона для -0.0 f (точное) в формате FP, мы могли бы отбросить кастинг до летучего двойника. (Но, возможно, сохранить float volatile, чтобы гарантировать, что с дезактивированными денормалами компилятор не может сделать какой-либо причудливый трюк, чтобы игнорировать операции, которые еще больше уменьшают абсолютное значение вещей, сравнивающихся с 0.0.)

Код может выглядеть так:

typedef union
{
  float fvalue;
  /* assuming int has at least the same number of bits as float */
  unsigned int bitpat;
} tBitAccess;

float my_signf(float x)
{

  volatile tBitAccess tmp;
  unsigned int pat0, patX;

  if (x < 0.0f) return -1.0f;
  if (x > 0.0f) return 1.0f;

  tmp.fvalue = -DBL_MIN;

  /* forcing something compares equal to 0.0f below smallest subnormal 
     - not sure if one abs()-factor is enough */
  tmp.fvalue = tmp.fvalue * fabsf(tmp.fvalue);
  pat0 = tmp.bitpat;

  tmp.fvalue = x; 
  tmp.fvalue = tmp.fvalue * fabsf(tmp.fvalue);
  patX = tmp.bitpat;

  return (patX == pat0)? -1.0f : 1.0f;

}

Это может не работать с причудливыми методами округления, которые предотвращают округление от отрицательных значений до -0.0.