Разрешена ли эта оптимизация с плавающей точкой? - программирование
Подтвердить что ты не робот

Разрешена ли эта оптимизация с плавающей точкой?

Я попытался проверить, где float теряет способность точно представлять большие целые числа. Итак, я написал этот маленький фрагмент:

int main() {
    for (int i=0; ; i++) {
        if ((float)i!=i) {
            return i;
        }
    }
}

Этот код работает со всеми компиляторами, кроме clang. Clang генерирует простой бесконечный цикл. Годболт

Это разрешено? Если да, это проблема QoI?

4b9b3361

Ответ 1

Как указывал @Angew, оператору != Необходим одинаковый тип с обеих сторон. (float)i != i приводит к продвижению RHS к float, поэтому мы имеем (float)i != (float)i.


g++ также генерирует бесконечный цикл, но он не оптимизирует работу изнутри. Вы можете видеть, что он конвертирует int-> float с помощью cvtsi2ss и делает ucomiss xmm0,xmm0 для сравнения (float)i с самим собой. (Это была ваша первая подсказка, что ваш источник C++ не означает, что вы думали, что он сделал, как объясняет ответ @Angew.)

x != x верно только тогда, когда он "неупорядочен", потому что x был NaN. (INFINITY сравнивается с равным себе в математике IEEE, но NaN нет. NAN == NAN - это ложь, NAN != NAN - это правда).

gcc7.4 и старше корректно оптимизируют ваш код для jnp как ветвь цикла (https://godbolt.org/z/fyOhW1): продолжайте цикл до тех пор, пока операнды x != x не были NaN. (gcc8 и более поздние версии также проверяют je на разрыв цикла, не в состоянии оптимизировать, основываясь на том факте, что это всегда будет верно для любого не-NaN-входа). x86 FP сравнивает установленный PF с неупорядоченным


И кстати, это означает, что оптимизация clang также безопасна: она просто должна CSE (float)i != (implicit conversion to float)i как то же самое, и доказать, что i → float никогда не NaN для возможного диапазона int,

(Хотя с учетом того, что этот цикл будет попадать в UB со ud2 переполнения, он позволял буквально испускать любой асм, который ему нужен, включая недопустимую инструкцию ud2 или пустой бесконечный цикл, независимо от того, каким на самом деле было тело цикла.) UB, эта оптимизация все еще на 100% законна.


GCC не удается оптимизировать тело цикла даже с помощью -fwrapv чтобы четко определить переполнение со -fwrapv со -fwrapv (в качестве дополнения в 2 дополнения). https://godbolt.org/z/t9A8t_

Даже включение -fno-trapping-math не помогает. (GCC по умолчанию, к сожалению, для включения
-ftrapping-math даже несмотря на то, что его реализация в GCC не работает или содержит ошибки.) int-> преобразование с плавающей точкой может вызвать неточное исключение FP (для чисел, слишком больших, чтобы быть представленными точно), поэтому с исключениями, возможно, не маскируемыми, разумно не оптимизировать тело циклы. (Поскольку преобразование 16777217 в 16777217 с плавающей запятой может иметь 16777217 побочный эффект, если неточное исключение не будет маскировано.)

Но с -O3 -fwrapv -fno-trapping-math 100% пропустили оптимизацию, чтобы не скомпилировать это в пустой бесконечный цикл. Без #pragma STDC FENV_ACCESS ON состояние липких флагов, которые записывают скрытые исключения FP, не является наблюдаемым побочным эффектом кода. Никакое преобразование intfloat может привести к NaN, поэтому x != x не может быть истинным.


Все эти компиляторы оптимизируют для реализаций C++, которые используют IEEE 754 с плавающей float одинарной точности (binary32) и 32-битное int.

Исправленный (int)(float)i != i цикл (int)(float)i != i будет иметь UB в реализациях C++ с узким 16-битным int и/или более широким float, потому что вы достигнете UB со знаком переполнения целых чисел, прежде чем достигнете первого целого числа, которое было Точно не может быть представлен в виде float.

Но UB при другом наборе вариантов реализации не имеет никаких негативных последствий при компиляции для реализации, такой как gcc или clang, с x86-64 System V ABI.


Кстати, вы можете статически вычислить результат этого цикла из FLT_RADIX и FLT_MANT_DIG, определенных в <climits>. Или, по крайней мере, теоретически, если float действительно соответствует модели поплавка IEEE, а не некоторому другому виду представления действительных чисел, например, Posit/unum.

Я не уверен, насколько стандарт ISO C++ ограничивается поведением с float и будет ли формат, не основанный на показателе фиксированной ширины и значимых полях, соответствовать стандартам.


В комментариях:

@geza Мне было бы интересно услышать полученный номер!

@nada: это 16777216

Вы утверждаете, что получили этот цикл для печати/возврата 16777216?

Обновление: так как этот комментарий был удален, я думаю, что нет. Вероятно, OP просто заключает в кавычки число с float перед первым целым числом, которое не может быть точно представлено как 32-разрядное число с float. https://en.wikipedia.org/wiki/Single-precision_floating-point_format#Precision_limits_on_integer_values то есть то, что они надеялись проверить с помощью этого глючного кода.

Версия с исправленной ошибкой, конечно, будет печатать 16777217, первое целое число, которое не может быть точно представлено, а не значение до этого.

(Все более высокие значения с плавающей запятой являются точными целыми числами, но они кратны 2, затем 4, затем 8 и т.д. Для значений показателей, больших, чем значимость и ширина. Могут быть представлены многие более высокие целочисленные значения, но 1 единица в последнем месте (из значимого) больше 1, поэтому они не являются смежными целыми числами. Самое большое конечное число с float чуть меньше 2 ^ 128, что слишком много даже для int64_t.)

Если какой-либо компилятор выйдет из исходного цикла и напечатает его, это будет ошибкой компилятора.

Ответ 2

Обратите внимание, что встроенный оператор != Требует, чтобы его операнды были одного и того же типа, и будет достигать этого с помощью повышений и преобразований, если это необходимо. Другими словами, ваше состояние эквивалентно:

(float)i != (float)i

Это никогда не должно заканчиваться ошибкой, и поэтому код в конечном итоге переполнится, i ваша программа будет иметь неопределенное поведение. Поэтому возможно любое поведение.

Чтобы правильно проверить, что вы хотите проверить, вы должны привести результат обратно к int:

if ((int)(float)i != i)