Почему Clang оптимизирует x * 1.0, но НЕ x + 0.0?

125

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

user541686
источник
2
Какие флаги оптимизации сейчас активны?
Iwillnotexist Idonotexist
1
@IwillnotexistIdonotexist: Я только что использовал -O3, но не знаю, как проверить, что это активирует.
user541686
2
Было бы интересно посмотреть, что произойдет, если вы добавите -ffast-math в командную строку.
plugwash
static double arr[N]не разрешено в C; constпеременные не считаются константными выражениями в этом языке
MM
1
[Вставьте язвительный комментарий о том, что C - это не C ++, даже если вы это уже назвали.]
user253751

Ответы:

164

Стандарт IEEE 754-2008 для арифметики с плавающей запятой и стандарт независимой от языка арифметики (LIA) ISO / IEC 10967, часть 1 дают ответы, почему это так.

IEEE 754 § 6.3 Знаковый бит

Когда вход или результат - NaN, этот стандарт не интерпретирует знак NaN. Однако обратите внимание, что операции с битовыми строками - copy, negate, abs, copySign - определяют бит знака результата NaN, иногда основанный на бите знака операнда NaN. На логический предикат totalOrder также влияет знаковый бит операнда NaN. Для всех других операций этот стандарт не определяет знаковый бит результата NaN, даже если имеется только одно входное NaN или когда NaN создается в результате недопустимой операции.

Когда ни входы, ни результат не являются NaN, знак произведения или частного является исключающим ИЛИ знаков операндов; знак суммы или разности x - y, рассматриваемой как сумма x + (−y), отличается не более чем от одного из знаков слагаемых; а знак результата преобразований, операции квантования, операций roundTo-Integral и roundToIntegralExact (см. 5.3.1) является знаком первого или единственного операнда. Эти правила применяются, даже если операнды или результаты равны нулю или бесконечны.

Когда сумма двух операндов с противоположными знаками (или разность двух операндов с одинаковыми знаками) равна нулю, знак этой суммы (или разности) должен быть +0 во всех атрибутах направления округления, кроме roundTowardNegative; под этим атрибутом знак точной нулевой суммы (или разности) должен быть -0. Однако x + x = x - (−x) сохраняет тот же знак, что и x, даже если x равен нулю.

Случай сложения

В режиме по умолчанию округление (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, то согласно

    IEEE 754 § 6.2.3 Распространение NaN

    Операция, которая распространяет операнд NaN на свой результат и имеет единственный NaN в качестве входных данных, должна давать NaN с полезной нагрузкой входного 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 есть следующая интересная цитата:

IEEE 754 § 10.4 Оптимизация буквального значения и изменения значений

[...]

Следующие трансформации, изменяющие значение, среди прочего, сохраняют буквальное значение исходного кода:

  • Применение свойства идентичности 0 + x, когда x не равно нулю и не является сигнальным NaN, и результат имеет тот же показатель степени, что и x.
  • Применение свойства идентичности 1 × x, когда x не является сигнальным NaN и результат имеет тот же показатель степени, что и x.
  • Изменение полезной нагрузки или бита знака тихого NaN.
  • [...]

Поскольку все 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 могут быть выбраны так : То есть, когда мы

  1. Соблюдайте рекомендацию передавать без изменений полезную нагрузку, xесли это NaN.
  2. Оставьте знаковый бит результата NaN неизменным на * 1.0 .
  3. Соблюдайте порядок XOR знакового бита во время частного / произведения, если xэто не NaN.

Чтобы включить оптимизацию IEEE-754-unsafe (x+0.0) -> x, -ffast-mathнеобходимо передать флаг в Clang или GCC.

Iwillnotexist Idonotexist
источник
2
Предостережение: что, если это сигнальный NaN? (На самом деле я подумал, что это могло быть причиной как-то, но я не знал, как это сделать, поэтому я спросил.)
user541686
6
@Mehrdad: Приложение F, (необязательная) часть стандарта C, которая определяет соответствие C IEEE 754, явно не распространяется на сигнальные NaN. (C11 F.2.1., Первая строка: «Эта спецификация не определяет поведение сигнализации NaN»). Реализации, декларирующие соответствие Приложению F, по-прежнему могут делать то, что они хотят, с сигнализацией NaN. Стандарт C ++ имеет собственную обработку IEEE 754, но что бы это ни было (я не знаком), я сомневаюсь, что он также определяет сигнальное поведение NaN.
user2357112 поддерживает Монику
2
@Mehrdad: sNaN вызывает неопределенное поведение в соответствии со стандартом (но, вероятно, оно хорошо определяется платформой), поэтому здесь разрешено сжатие компилятора.
Джошуа
1
@ user2357112: Возможность перехвата ошибок в качестве побочного эффекта для других неиспользуемых вычислений обычно мешает оптимизации; если результат вычисления иногда игнорируется, компилятор может с пользой отложить вычисление до тех пор, пока не узнает, будет ли использован результат, но если вычисление дало бы важный сигнал, это может быть плохо.
supercat
2
О, смотрите, вопрос, который законно относится как к C, так и к C ++, на который точно дан ответ для обоих языков ссылкой на один стандарт. Уменьшит ли это вероятность того, что люди будут жаловаться на вопросы, помеченные как C, так и C ++, даже если вопрос касается общности языка? К сожалению, я думаю, что нет.
Кайл Стрэнд
35

x += 0.0не NOOP, если xесть -0.0. Оптимизатор в любом случае может вырезать весь цикл, поскольку результаты не используются. В общем, трудно сказать, почему оптимизатор принимает такие решения.

user2357112 поддерживает Монику
источник
2
На самом деле я опубликовал это после того, как только что прочитал, почему x += 0.0это не бездействие, но я подумал, что, вероятно, это не причина, потому что весь цикл должен быть оптимизирован в любом случае. Я могу купить это, просто это не так убедительно, как я надеялся ...
user541686
Учитывая склонность объектно-ориентированных языков к возникновению побочных эффектов, я мог бы предположить, что было бы трудно быть уверенным, что оптимизатор не меняет фактическое поведение.
Роберт Харви,
Может быть причина, так как с long longоптимизацией действует (сделал это с gcc, который ведет себя так же, как минимум для дубля )
e2-e4
2
@ ringø: long longинтегральный тип, а не тип IEEE754.
MSalters
1
А как насчет x -= 0того же?
Виктор Меллгрен,