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

Почему GCC не может оптимизировать `std:: sqrt`?

У меня есть простая программа:

#include <cmath>

int main()
{
    for (int i = 0; i < 50; ++i)
        std::sqrt(i);
}

Clang 3.8 оптимизирует его в -O3, но gcc 6.1. Он создает следующую сборку:

## Annotations in comments added after the question was answered,
## for the benefit of future readers.
main:
    pushq   %rbx
    xorl    %ebx, %ebx
    jmp     .L2
.L4:
    pxor    %xmm0, %xmm0         # break cvtsi2sd false dep on the old value of xmm0
    pxor    %xmm1, %xmm1         # xmm1 = 0.0
    cvtsi2sd        %ebx, %xmm0  # xmm0 = (double)i
    ucomisd %xmm0, %xmm1         # scalar double comparison, setting flags
    ja      .L7                  # if (0.0 > (double)i) sqrt(i);   // The `a` = above.  AT&T syntax reverses the order, but it jump if  xmm1 above xmm0
.L2:
    addl    $1, %ebx             # i++
    cmpl    $50, %ebx
    jne     .L4                  # i != 50
    xorl    %eax, %eax
    popq    %rbx
    ret                          # return 0
.L7:
    call    sqrt                 # only executed on i < 0.  Otherwise gcc knows std::sqrt has no side effects.
    jmp     .L2

Если я правильно понимаю правило as-if, компилятору разрешено оптимизировать код, который не изменяет наблюдаемое поведение программы, которое включает в себя операции ввода-вывода и т.д. Я отказываюсь от результата std::sqrt и не выполняйте ввода-вывода. Кроме того, у меня нет #pragma STDC FENV_ACCESS в моей программе. Имеет ли std::sqrt наблюдаемые побочные эффекты или есть еще одна причина, по которой GCC не оптимизирует вызов?


(Первоначальная версия этого вопроса имела верхнюю границу 10e50, делая ее бесконечным циклом. То же самое происходит 50, поэтому nvm комментарии об этом.)

4b9b3361

Ответ 1

Это несколько связано с разворачиванием цикла.

int main()
{
  for (int i = 0; i <= 16; ++i)  // CHANGED NUMBER OF ITERATIONS
    std::sqrt(i);
}

заменяется на a return 0; (g++ -O3 -fdump-tree-all).

Если вы посмотрите на .115t.cunroll, вы увидите, что код изначально преобразуется в нечто вроде:

// ...

<bb 6>:
i_30 = i_22 + 1;
_32 = (double) i_30;
if (_32 < 0.0)
  goto <bb 7>;
else
  goto <bb 8>;

<bb 7>:
__builtin_sqrt (_32);

<bb 8>:
i_38 = i_30 + 1;
_40 = (double) i_38;
if (_40 < 0.0)
  goto <bb 9>;
else
  goto <bb 10>;

<bb 9>:
__builtin_sqrt (_40);

// ...

а компилятор с фактическими числами может "доказать", что каждый вызов sqrt не имеет побочных эффектов (.125t.vrp2):

// ...

<bb 6>:
i_30 = 3;
_32 = 3.0e+0;
if (_32 < 0.0)
  goto <bb 7>;
else
  goto <bb 8>;

<bb 7>:
__builtin_sqrt (_32);

<bb 8>:
i_38 = 4;
_40 = 4.0e+0;
if (_40 < 0.0)
  goto <bb 9>;
else
  goto <bb 10>;

<bb 9>:
__builtin_sqrt (_40);

// ...

Если число итераций велико, gcc:

  • не выполняет циклическую развертку (если не принудительно с чем-то вроде --param max-completely-peeled-insns=x --param max-completely-peel-times=y)
  • недостаточно "достаточно умен", чтобы определить, что вызов sqrt(i) не имеет побочных эффектов (но достаточно небольшой справки, например std::sqrt(std::abs(i))).

Также gcc (v6.x) не поддерживает #pragma STDC FENV_ACCESS , поэтому он должен предположить, что эта прагма включена (иначе сгенерированный код может быть некорректным) (ситуация более сложная, см. ошибка 34678 и комментарий Тавиана Барнса).

Ответ 2

Причина в том, что стандарту требуется установить errno, если sqrt передано отрицательное число. По-видимому, для значения 50 (слишком много для разворачивания) g++ "забывает", что отрицательные значения невозможны, а для небольших значений вместо этого разворачивается и обнаруживается в каждом отдельном случае постоянным распространением, что не может возникнуть необходимость установки errno.

Также, принимая абсолютное значение перед вызовом sqrt, очевидно, гарантирует g++, что невозможно передать отрицательный аргумент.