Я попытался проверить, где float
теряет способность точно представлять большие целые числа. Итак, я написал этот небольшой фрагмент:
int main() {
for (int i=0; ; i++) {
if ((float)i!=i) {
return i;
}
}
}
Этот код работает со всеми компиляторами, кроме clang. Clang генерирует простой бесконечный цикл. Godbolt .
Это разрешено? Если да, то это проблема QoI?
c++
floating-point
clang
геза
источник
источник
gcc
выполняет ту же оптимизацию с бесконечными циклами, если вы-Ofast
вместо этого компилируете , поэтому такая оптимизацияgcc
считается небезопасной, но может это сделать.ucomiss xmm0,xmm0
сравнить это(float)i
с собой. Это была ваша первая подсказка о том, что ваш исходный код на C ++ не означает то, что вы думали. Вы утверждаете, что у вас есть этот цикл для печати / возврата16777216
? С каким компилятором / версией / параметрами это было? Потому что это будет ошибка компилятора. gcc правильно оптимизирует ваш кодjnp
как ветвь цикла ( godbolt.org/z/XJYWeu ): продолжайте цикл до тех пор, пока операнды!=
не были NaN.-ffast-math
опция, которая неявно включена,-Ofast
что позволяет GCC применять небезопасные оптимизации с плавающей запятой и, таким образом, генерировать тот же код, что и Clang. MSVC ведет себя точно так же: без/fp:fast
него он генерирует кучу кода, который приводит к бесконечному циклу; с/fp:fast
, он выдает однуjmp
инструкцию. Я предполагаю, что без явного включения небезопасных оптимизаций FP эти компиляторы будут зависеть от требований IEEE 754 в отношении значений NaN. Довольно интересно, что Clang этого не делает. Его статический анализатор лучше. @ 12345ieee(float) i
отличается от математического значенияi
, то результат (значение, возвращаемое вreturn
операторе) будет 16 777 217, а не 16 777 216.Ответы:
Как отметил @Angew ,
!=
оператору нужен один и тот же тип с обеих сторон.(float)i != i
приводит к продвижению RHS в плавание, так что мы и сделали(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 с переполнением со знаком, ему разрешено испускать буквально любой asm, который он хочет, включая
ud2
недопустимую инструкцию или пустой бесконечный цикл, независимо от того, каким было тело цикла на самом деле.) Но игнорирование UB с переполнением со знаком. , эта оптимизация на 100% легальна.GCC не может оптимизировать тело цикла даже с тем,
-fwrapv
чтобы сделать целочисленное переполнение со знаком четко определенным (как двойное дополнение). https://godbolt.org/z/t9A8t_Даже включение
-fno-trapping-math
не помогает. (По умолчанию GCC, к сожалению, включен,-ftrapping-math
хотя его реализация в GCC не работает / содержит ошибки .) Преобразование int-> float может вызвать неточное исключение FP (для чисел, слишком больших для точного представления), поэтому с исключениями, которые могут быть размаскированы, разумно не оптимизировать тело цикла. (Поскольку преобразование16777217
в число с плавающей точкой может иметь заметный побочный эффект, если неточное исключение разоблачено.)Но с
-O3 -fwrapv -fno-trapping-math
, это 100% упущенная оптимизация, чтобы не компилировать это в пустой бесконечный цикл. Без#pragma STDC FENV_ACCESS ON
него состояние липких флагов, которые записывают замаскированные исключения FP, не является наблюдаемым побочным эффектом кода. Нетint
->float
преобразование может привести к NaN, поэтомуx != x
не может быть правдой.Все эти компиляторы оптимизированы для реализаций C ++, использующих одинарную точность IEEE 754 (binary32)
float
и 32-разрядныеint
.Цикл с исправленной ошибкой
(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
поведении и будет ли формат, не основанный на полях экспоненты фиксированной ширины и значимости, соответствовать стандартам.В комментариях:
Вы утверждаете, что у вас есть этот цикл для печати / возврата
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
.)Если какой-либо компилятор выйдет из исходного цикла и напечатает его, это будет ошибкой компилятора.
источник
frapw
, но я уверен, что GCC 10-ffinite-loops
был разработан для таких ситуаций.Обратите внимание, что для встроенного оператора
!=
требуется, чтобы его операнды были одного и того же типа, и при необходимости для этого используются рекламные акции и преобразования. Другими словами, ваше состояние эквивалентно:(float)i != (float)i
Это никогда не должно привести к сбою, и поэтому код в конечном итоге переполнится
i
, что приведет к неопределенному поведению вашей программы. Поэтому возможно любое поведение.Чтобы правильно проверить, что вы хотите проверить, вы должны вернуть результат в
int
:if ((int)(float)i != i)
источник
static_cast<int>(static_cast<float>(i))
?reinterpret_cast
очевидно UB там(int)(float)i != i
это UB? Как вы пришли к такому выводу? Да, это зависит от свойств, определенных реализацией (потому чтоfloat
это не обязательно, чтобы быть двоичным 32 по стандарту IEEE754), но для любой данной реализации он четко определен, если неfloat
может точно представлять все положительныеint
значения, поэтому мы получаем целочисленное переполнение со знаком UB. ( en.cppreference.com/w/cpp/types/climits определяетFLT_RADIX
иFLT_MANT_DIG
определяет это). В общем, печать определяется реализацией, напримерstd::cout << sizeof(int)
, не UB ...reinterpret_cast<int>(float)
это не совсем UB, это просто синтаксическая ошибка / неверный формат . Было бы неплохо, если бы этот синтаксис позволял использовать тип floatint
в качестве альтернативыmemcpy
(который четко определен), ноreinterpret_cast<>
, я думаю, работает только с типами указателей.x != x
это правда. Смотри вживую по колиру . В Си тоже.