Почему 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 комментарии об этом.)
Ответы
Ответ 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++, что невозможно передать отрицательный аргумент.