Почему Clang оптимизирует цикл в этом коде
#include <time.h>
#include <stdio.h>
static size_t const N = 1 << 27;
static double arr[N] = { /* initialize to zero */ };
int main()
{
clock_t const start = clock();
for (int i = 0; i < N; ++i) { arr[i] *= 1.0; }
printf("%u ms\n", (unsigned)(clock() - start) * 1000 / CLOCKS_PER_SEC);
}
а не цикл в этом коде?
#include <time.h>
#include <stdio.h>
static size_t const N = 1 << 27;
static double arr[N] = { /* initialize to zero */ };
int main()
{
clock_t const start = clock();
for (int i = 0; i < N; ++i) { arr[i] += 0.0; }
printf("%u ms\n", (unsigned)(clock() - start) * 1000 / CLOCKS_PER_SEC);
}
(Пометка как C, так и C ++, потому что я хотел бы знать, отличается ли ответ для каждого.)
c++
c
optimization
floating-point
clang
user541686
источник
источник
-O3
, но не знаю, как проверить, что это активирует.static double arr[N]
не разрешено в C;const
переменные не считаются константными выражениями в этом языкеОтветы:
Стандарт IEEE 754-2008 для арифметики с плавающей запятой и стандарт независимой от языка арифметики (LIA) ISO / IEC 10967, часть 1 дают ответы, почему это так.
Случай сложения
В режиме по умолчанию округление (Round-к-ближняя, Галстуки-к-четно) , мы видим , что
x+0.0
производитx
, кроме случаев , когдаx
это-0.0
: В этом случае мы имеем сумму двух операндов с противоположными знаками, сумма которых равна нулю, а в пункте §6.3 3 правила, которые дает это дополнение+0.0
.Так
+0.0
как не является побитовым идентичным оригиналу-0.0
и-0.0
это допустимое значение, которое может быть введено в качестве входных данных, компилятор обязан ввести код, который преобразует потенциальные отрицательные нули в+0.0
.Сводка: в режиме округления по умолчанию, в
x+0.0
, еслиx
-0.0
, тоx
сам по себе является приемлемым выходным значением.-0.0
, то выходное значение должно быть+0.0
, что не побитово идентично-0.0
.Случай умножения
В режиме округления по умолчанию такой проблемы не возникает с
x*1.0
. Еслиx
:x*1.0 == x
всегда является (суб) нормальным числом .+/- infinity
, то результат+/- infinity
того же знака.есть
NaN
, то согласночто означает , что показатель и мантисса (хотя и не знак)
NaN*1.0
являются рекомендуется , чтобы быть неизменными от входаNaN
. Знак не указан в соответствии с §6.3p1 выше, но реализация может указать, что он идентичен источникуNaN
.+/- 0.0
, то результатом будет0
знаковый бит, объединенный XOR со знаковым битом1.0
, в соответствии с §6.3p2. Поскольку знаковый бит1.0
равен0
, выходное значение не отличается от входного. Таким образом,x*1.0 == x
даже когдаx
является (отрицательным) нулем.Случай вычитания
В режиме округления по умолчанию вычитание
x-0.0
также не выполняется, потому что оно эквивалентноx + (-0.0)
. Еслиx
этоNaN
, то §6.3p1 и §6.2.3 применяются почти так же, как для сложения и умножения.+/- infinity
, то результат+/- infinity
того же знака.x-0.0 == x
всегда .-0.0
, то согласно §6.3p2 мы имеем « [...] знак суммы или разности x - y, рассматриваемой как сумма x + (−y), отличается не более чем от одного из знаков слагаемых; ». Это заставляет нас присваивать-0.0
как результат(-0.0) + (-0.0)
, потому что не-0.0
отличается по знаку ни от одного слагаемого, а+0.0
по знаку отличается от двух слагаемых в нарушение этого пункта.+0.0
, то это сводится к случаю сложения ,(+0.0) + (-0.0)
рассмотренной выше в случае добавления , который по §6.3p3 правят , чтобы дать+0.0
.Поскольку для всех случаев входное значение является допустимым в качестве выходного, допустимо рассматривать
x-0.0
отсутствие операции иx == x-0.0
тавтологию.Оптимизация, меняющая ценность
В стандарте IEEE 754-2008 есть следующая интересная цитата:
Поскольку все NaN и все бесконечности имеют один и тот же показатель степени, и правильно округленный результат
x+0.0
иx*1.0
для конечногоx
имеет точно такую же величину, что иx
, их показатель степени такой же.sNaNs
Сигнальные NaN - это значения прерывания с плавающей запятой; Это особые значения NaN, использование которых в качестве операнда с плавающей запятой приводит к исключению недопустимой операции (SIGFPE). Если бы цикл, запускающий исключение, был оптимизирован, программное обеспечение больше не работало бы так же.
Однако, как указывает user2357112 в комментариях , стандарт C11 явно оставляет неопределенным поведение сигнализации NaNs (
sNaN
), поэтому компилятору разрешено предполагать, что они не происходят, и, таким образом, исключения, которые они вызывают, также не возникают. Стандарт C ++ 11 опускает описание поведения для сигнализации NaN и, таким образом, также оставляет его неопределенным.Режимы округления
В альтернативных режимах округления допустимые оптимизации могут измениться. Например, в режиме Round-to-Negative-Infinity оптимизация
x+0.0 -> x
становится допустимой, ноx-0.0 -> x
становится запрещенной.Чтобы предотвратить использование GCC режимов округления и поведения по умолчанию, экспериментальный флаг
-frounding-math
можно передать GCC.Вывод
Clang и GCC , даже в
-O3
, остаются совместимыми с IEEE-754. Это означает, что он должен соответствовать вышеуказанным правилам стандарта IEEE-754.x+0.0
это не бит идентичен сx
для всех вx
соответствии с этими правилами, ноx*1.0
могут быть выбраны так : То есть, когда мыx
если это NaN.* 1.0
.x
это не NaN.Чтобы включить оптимизацию IEEE-754-unsafe
(x+0.0) -> x
,-ffast-math
необходимо передать флаг в Clang или GCC.источник
x += 0.0
не NOOP, еслиx
есть-0.0
. Оптимизатор в любом случае может вырезать весь цикл, поскольку результаты не используются. В общем, трудно сказать, почему оптимизатор принимает такие решения.источник
x += 0.0
это не бездействие, но я подумал, что, вероятно, это не причина, потому что весь цикл должен быть оптимизирован в любом случае. Я могу купить это, просто это не так убедительно, как я надеялся ...long long
оптимизацией действует (сделал это с gcc, который ведет себя так же, как минимум для дубля )long long
интегральный тип, а не тип IEEE754.x -= 0
того же?