Подтвердить что ты не робот

Неправильно ли эта оптимизация GCC?

У меня есть функтор, который принимает значение, отбрасывает его в double, берет журнал и возвращает значение обратно в исходный тип. Для целей этого вопроса исходный и выходной тип - float. Вот исходный код на С++:

return static_cast< TOutput >( std::log( static_cast< double >( A ) ) )

Когда я компилирую в режиме отладки, все идет так, как ожидалось, и GCC вызывает базовую функцию log:

  51:/myfile.h ****     return static_cast< TOutput >( std::log( static_cast< double >( A ) ) );
 219133                 .loc 112 51 0
 219134 0010 488B45F0     movq  -16(%rbp), %rax # A, tmp64
 219135 0014 F30F1000     movss (%rax), %xmm0 # *A_1(D), D.237346
 219136 0018 0F14C0       unpcklps  %xmm0, %xmm0  # D.237346, D.237346
 219137 001b 0F5AC0       cvtps2pd  %xmm0, %xmm0  # D.237346, D.237347
 219138 001e E8000000     call  log #
 219138      00
 219139 0023 660F14C0     unpcklpd  %xmm0, %xmm0  # D.237347
 219140 0027 660F5AC0     cvtpd2ps  %xmm0, %xmm0  # D.237347, D.237346
 219141 002b F30F1145     movss %xmm0, -20(%rbp)  # D.237346, %sfp
 219141      EC
 219142 0030 8B45EC       movl  -20(%rbp), %eax # %sfp, <retval>

Однако, когда я включаю оптимизации на (-O2 -ggdb3 -DNDEBUG), он вызывает функцию logf (???):

  51:/myfile.h ****     return static_cast< TOutput >( std::log( static_cast< double >( A ) ) );
 145171                 .loc 64 51 0
 145172 01a0 F30F1004     movss (%rdx,%rax,4), %xmm0  # MEM[(const float &)_84], MEM[(const float &)_84]
 145172      82         
 145173 01a5 E8000000     call  logf  #

Он дает другой вывод. Это нормально? Я что-то делаю неправильно? Мне кажется, что GCC очень либерально интерпретирует мой код, чего я не ожидал бы в отсутствие опции -ffast-math.

4b9b3361

Ответ 1

Это пограничная оптимизация для преобразования преобразования в float приложения двойной точности log в float для приложения с одной точностью log, но можно утверждать, что она приемлема.

Предполагая, что logf правильно округлено и что двойная точность log также правильно закруглена или, по крайней мере, точно округлена, эти два вычисления будут редко отличаться. Они могут отличаться (для некоторых редких входов) из-за двойного округления (в котором "double" означает "дважды" и не ссылается на тип). Двойное округление статистически менее значимо, поскольку в промежуточном типе значимости есть дополнительные цифры по сравнению с синтаксисом конечного типа (и этот статистический аргумент является слегка мусором с математической точки зрения, но он "работает на практике" для функций, которые не были разработаны, чтобы быть контрпримерами). По дидактическим причинам люди (или Википедия) объясняют это одним или двумя дополнительными цифрами точности, но когда у вас есть 53 - 24 = 29 дополнительных двоичных цифр, можно ожидать, что это произойдет редко, как раз в 2 29.

Меня удивляет оптимизация, и я был бы обеспокоен этим, если бы сам написал код для исчерпывающего поиска проблем двойного округления с log, но учитывая, что стандарт С++ не требует какого-либо уровня точности от std::log, это можно считать "не ошибкой".


Если вместо log мы говорили об одной из основных операций (например, *), то преобразование было бы неверным, поскольку компилятор, который утверждает, что предоставляет семантику IEEE 754, когда это вводит видимые изменения. Для базовой операции точность указана косвенно IEEE 754, и спецификация не оставляет места для изменений.

Так получилось, что для основных операций не может быть видимого изменения, когда floatOP(flx,fly) заменяет (float)doubleOP((double)flx, (double)fly) (этот тезис демонстрирует это в главе 6), но могут быть видимые различия, когда типы double и long double. Эта точная ошибка была недавно исправлена ​​ в Clang by Stephen Canon.

Ответ 2

Да, эта оптимизация неверна. log и logf должны быть обязательно округлены, поэтому можно было бы

logf(4) = 0x1.62e42p+0
log(4)  = 0x1.62e42fefa39efp+0

Изменение upconvert, log и downconversion на вызов logf могут давать неверные результаты.

Ответ Паскаля Куока правильно указывает, что если logf правильно округлено, а log - не мусор, то результаты, вероятно, не будут отличаться. Однако logf на моей платформе не правильно округлен:

logf(0x1.306p-138)       = -0x1.7decc8p+6
(float)log(0x1.306p-138) = -0x1.7decc6p+6
mpfr_log(0x1.306p-138)   = -0x1.7decc6ff8a7a4a4450e9p+6

К счастью, я не могу воспроизвести эту "оптимизацию" с помощью моего gcc.

Ответ 3

Я пробовал эквивалентную программу на C:

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


static double dostuff(float in)
{
  return log(in);
}

int main(int argc, char **argv)
{
  if (argc < 2)
    exit(EXIT_FAILURE);
  float in = strtof(argv[1], NULL);
  float out = dostuff(in);
  printf("%f\n", out);
  return 0;
}

И обнаружил, что gcc не использует logf даже при использовании -Ofast. Единственное, что позволяет -Ofast использовать __log_finite(). Однако изменение типа возврата dostuff() в float позволяет эту оптимизацию.

Ответ 4

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

Если вы беспокоитесь о точности операции, вам не следует использовать тип float вообще, и все ваши математические значения должны быть в два раза выше.

Используя тип float, вы сигнализируете компилятору, что у вас нет никакого особого внимания к конечной точности ваших математических операций, чем конечная точность, предоставляемая типом float. Другими словами, когда вы выполняете некоторые математические операции с действительными числами (числами, отличными от целых), точность может быть либо либо одинаковой, либо уменьшаться; он никогда не может увеличиться; поэтому, если вы начинаете с типа float, вы не можете увеличить свою точность за пределами этого типа.