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

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

Ответ 1

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

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

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

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

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

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

В режиме округления по умолчанию (от округления до ближайшего, от Ties-to-Even) мы видим, что x+0.0 создает x, EXCEPT, когда x - -0.0: В этом случае мы имеем сумму двух операндов с противоположными знаками, сумма которых равна нулю, а в параграфе 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 всегда.
  • is +/- infinity, тогда результат равен +/- infinity того же знака.
  • есть NaN, то согласно

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

    Операция, которая распространяет операнд NaN на его результат и имеет один NaN в качестве входного сигнала, должна создавать NaN с полезной нагрузкой входного NaN, если она представлена ​​в целевом формате.

    что означает, что экспонента и мантисса (хотя и не знак) NaN*1.0 рекомендуется не изменять из ввода NaN. Знак не указан в соответствии с §6.3p1 выше, но реализация может указывать, что он идентичен источнику NaN.

  • is +/- 0.0, то результатом является 0 со своим знаковым битом XORed со знаком-битом 1.0, что согласуется с §6.3p2. Поскольку знаковый бит 1.0 равен 0, выходное значение не изменяется от входа. Таким образом, x*1.0 == x, даже когда x является (отрицательным) нулем.

Случай вычитания

В режиме округления по умолчанию вычитание x-0.0 также не работает, потому что оно эквивалентно x + (-0.0). Если x

  • is NaN, то §6.3p1 и §6.2.3 применяются во многом так же, как для сложения и умножения.
  • is +/- 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), рассмотренному выше в "Дополнении", который согласно § 3.6.33 имеет значение +0.0.

Так как для всех случаев входное значение является законным как выход, допустимо рассматривать x-0.0 a no-op и x == x-0.0 тавтологию.

Оптимизация изменения стоимости

В стандарте IEEE 754-2008 есть следующая интересная цитата:

IEEE 754 § 10.4 Литеральное значение и оптимизация изменения стоимости

[...]

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

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

Так как все NaN и все бесконечности имеют один и тот же показатель, а правильно округленный результат x+0.0 и x*1.0 для конечного x имеет точно такую ​​же величину, что и x, их показатель один и тот же.

sNaNs

Сигнальные NaN - значения ловушки с плавающей запятой; Это специальные значения NaN, использование которых в качестве операнда с плавающей запятой приводит к недопустимому операционному исключению (SIGFPE). Если цикл, инициирующий исключение, был оптимизирован, программное обеспечение больше не будет вести себя одинаково.

Однако, как указывает user2357112 в комментариях, стандарт C11 явно оставляет undefined поведение сигналов NaNs (sNaN), поэтому компилятор позволили предположить, что они не происходят, и, следовательно, исключения, которые они поднимают, также не происходят. Стандартный стандарт С++ 11 описывает поведение для сигнализации NaN и, следовательно, также оставляет его undefined.

Режимы округления

В альтернативных режимах округления допустимые оптимизации могут измениться. Например, в режиме От круглой до отрицательной бесконечности оптимизация 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.
  • Оставьте знаковый бит результата NaN без изменений * 1.0.
  • Соблюдайте порядок XOR бита знака во время quotient/product, когда x не является NaN.

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

Ответ 2

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