Я наткнулся на функцию, которая мне кажется ненужной и вообще меня пугает:
float coerceToFloat(double x) {
volatile float y = static_cast<float>(x);
return y;
}
Что затем используется следующим образом:
// double x
double y = coerceToFloat(x);
Это когда-нибудь отличается от простого выполнения этого ?:
double y = static_cast<float>(x);
Похоже, что цель состоит в том, чтобы просто сократить двойную точность до одинарной точности. Пахнет чем-то написанным из-за крайней паранойи.
Понятия не имею, почему автор кода использовал переменную volatile. Насколько мне известно, функция ничем не отличается от float coerceToFloat(double x) { return static_cast<float>(x); }.
Я имею в виду, это хорошая практика - дать этой операции имя. coerceToFloat, безусловно, гораздо более четко описывает намерение, чем простое статическое приведение. Летучие ... Хм. Может для отладки?
@MaxLanghof это так, программирование наугад не работает
Возможно, я нашла крошку хлеба. Этот говорит, что использование volatile может нарушить операции с плавающей запятой. Возможно, автор использовал его по той же причине, он заставляет компилятор усекать здесь, вместо того, чтобы оптимизировать его и не выдавать промежуточный результат.
ой! @NathanOliver, тогда volatile используется для предотвращения оптимизации, которая может помешать коду выполнять то, для чего он был с отступом! потрясающая находка!
Я не думаю, что стандарт гарантирует это см. несколько связанный вопрос
Да, я думаю, что volatile должен сказать: «Вы должны подогнать его под настоящий float, прежде чем продолжить». Я вспоминал ответ на другой мой вопрос, который утверждает: «Стандарт C++ требует, чтобы избыточная точность была отброшена в присваиваниях и приведениях». Я вспомнил часть этого «в назначениях», которая заставляет меня думать, что volatile не является необходимым (достаточно просто присвоения именованного числа с плавающей запятой), но забыл часть «и приводит», которая, кажется, отвечает на этот вопрос. Хотя на самом деле это не относилось к стандарту.
@thoron volatile означает, что объект должен быть представлен так, как ожидает ABI, а не так, как оптимизатор хотел бы наиболее эффективно представить его.
@Ben Присваивания и преобразования могут исчезнуть после компиляции в промежуточный код. Внутренний язык компилятора, вероятно, не имеет концепции приведения, поскольку это не семантическая конструкция в нормальном программировании (расслабленная семантика произвольной / случайной плавающей запятой - это ненормальное программирование). Авторы компиляторов часто прямо отказываются реализовать часть языковой семантики, которую они считают черным, например, правило видимого объединения в C.





В некоторых компиляторах используется концепция «расширенной точности», когда двойники несут с собой более 64 бит данных. Это приводит к вычислениям с плавающей запятой, которые не соответствуют стандарту IEEE.
Приведенный выше код может быть попыткой помешать флагам расширенной точности компилятора удалить потерю точности. Такие флаги явно нарушают предположения о точности значений типа double и с плавающей запятой. Кажется правдоподобным, что они не сделали бы этого с переменной volatile.
Следуя комментарию @NathanOliver, компиляторам разрешено выполнять математические вычисления с плавающей запятой с более высокой точностью, чем того требуют типы операндов. Обычно на x86 это означает, что они делают все как 80-битные значения, потому что это наиболее эффективно для оборудования. Только когда значение сохраняется, его нужно вернуть к фактической точности типа. И даже в этом случае большинство компиляторов по умолчанию будут выполнять оптимизацию, которая нарушает это правило, потому что принудительное изменение точности замедляет операции с плавающей запятой. В большинстве случаев это нормально, потому что дополнительная точность не вредна. Если вы приверженец, вы можете использовать переключатель командной строки, чтобы заставить компилятор соблюдать это правило хранения, и вы можете увидеть, что ваши вычисления с плавающей запятой значительно медленнее.
В этой функции отметка переменной volatile сообщает компилятору, что он не может отказаться от сохранения этого значения; это, в свою очередь, означает, что он должен уменьшить точность входящего значения, чтобы соответствовать типу, в котором оно хранится. Так что есть надежда, что это приведет к усечению.
И нет, писать приведение вместо вызова этой функции - это не одно и то же, потому что компилятор (в своем несоответствующем режиме) может пропустить присвоение y, если он определит, что может сгенерировать лучший код без сохранения значения, и он также можно пропустить усечение. Имейте в виду, что цель состоит в том, чтобы выполнять вычисления с плавающей запятой как можно быстрее, и необходимость иметь дело с мелкими правилами о снижении точности для промежуточных значений просто замедляет работу.
В большинстве случаев серьезным приложениям, работающим с плавающей запятой, нужна полная работа за счет пропуска промежуточных усечений. Правило, требующее усечения для хранения, - это скорее надежда, чем реальное требование.
Кстати, в Java изначально требовалось, чтобы все вычисления с плавающей запятой выполнялись с точной точностью, необходимой для соответствующих типов. Вы можете сделать это на оборудовании Intel, сказав ему не расширять типы fp до 80 бит. Это было встречено громкими жалобами со стороны вычислителей чисел, потому что это замедляет вычисления много. Вскоре Java изменилась на понятие «строгий» fp и «нестрогий» fp, а при серьезном вычислении чисел используется нестрогий, то есть сделать его настолько быстрым, насколько поддерживает оборудование. Люди, которые досконально разбираются в математике с плавающей запятой (в том числе и я в нет), хотят скорости и знают, как справиться с возникающими различиями в точности.
«Обычно на x86 это означает, что они делают все как 80-битные значения, потому что это наиболее эффективно для оборудования» - сомнительно. Некоторые компиляторы, возможно, использовали 80-битные регистры с плавающей запятой в прошлом, но конструкции процессоров изменились, и теперь есть недостатки в использовании этих старых регистров и их операций.
@EricPostpischil Я еще не видел соответствующая реализация. Много времени они будут несоответствовать, чтобы работать лучше, с флагом, который будет делать то, что говорит стандарт, но за вашу цену.
@NathanOliver: Покажите нам пример кода с реализацией, которая не устраняет избыточную точность, когда приведение или присваивание выполняется вместе со сборкой, созданной этой реализацией.
@EricPostpischil - вы правы, что 80-битные вещи - это эра неандертальцев. Я все время забываю, что то, что я узнал 20 лет назад, не обязательно является современным.
@EricPostpischil - re: «Соответствующая реализация C++ не может ...» действительно. Думаю, я сказал это несколько раз достаточно четко.
@PeteBecker: язык C был разработан на основе предположения, что тип, используемый для аргумента с плавающей запятой, не зависит от выбора типов с плавающей запятой, используемых в выражении. Единственная причина, по которой 80-битные значения являются «первобытными», заключается в том, что C89 не предоставляет средства, с помощью которых реализации могли бы предоставлять тип длиннее, чем double, используемый для временной оценки выражений, без фундаментального нарушения языка.
@PeteBecker: Я думаю, что нестрогий fp в большинстве случаев по-прежнему требует округления до точности double. Единственное место, где разрешено отличаться, - это денормализованные числа. Чтобы использовать десятичную аналогию, если точность составляет 3 цифры, а минимальный показатель степени равен -9, точное значение умножения 5,26E-9 на 2,0E-2 будет 1,052E-10, которое следует округлить до 0,11E-9. Однако округление до трех цифр без усечения экспоненты даст 1,05E-10, которое затем будет округлено до 0,10E-9. Гарантировать правильное округление в этом случае сложно для оборудования ...
... который не сочетает округление и денормализацию на одном этапе и для большинства целей не принесет никакой выгоды, достаточной для оправдания огромных дополнительных затрат.
@supercat - вздох; в кроличью нору. Strict-fp в Java требует, чтобы вычисления, включающие два типа double, выполнялись с точностью до double. Не позволяет производить расчет на 80 битах и округлять результат в два раза; что может дать другой результат.
@PeteBecker: Strict-fp требует этого, но даже нестрогий фп по-прежнему требует такого поведения в случаях, которые не связаны с денормальными значениями.
static_cast<float>(x) требуется для удаления лишней точности, в результате чего получается float. Хотя стандарт C++ обычно позволяет реализациям сохранять избыточную точность с плавающей запятой в выражениях, эта точность должна быть устранена операторами приведения и присваивания.
Лицензия на использование большей точности содержится в пункте 13 статьи 8 проекта N4659 C++:
The values of the floating operands and the results of floating expressions may be represented in greater precision and range than that required by the type; the types are not changed thereby.64
В сноске 64 говорится:
The cast and assignment operators must still perform their specific conversions as described in 8.4, 8.2.9 and 8.18.
Я не уверен, что это правильно. Сноски не являются нормативными. Возможно, цель этой сноски - сказать, что с помощью приведения / присвоения можно изменить тип. Но излишняя точность может остаться. Это проблема качества реализации, поэтому компиляторы, вероятно, теряют дополнительную точность, но они не обязаны этого делать. Я не говорю, что это правильная интерпретация, но мне кажется правдоподобной.
@geza: сноски не являются нормативными, но такое поведение хорошо известно для C и C++; есть и другие вопросы о переполнении стека и ответы на него. Хотя сноска не является нормативной, она информирует нас о том, что правильная интерпретация нормативного текста об операторах приведения состоит в том, что они действительно выполняют преобразование, которое они говорят, что они делают, т.е., преобразование из double в float на самом деле создает float.
@EricPostpischil: Я не думаю, что авторы думали, что это будет иметь особое значение, допускает ли Стандарт технически поведение, которое противоречит общим ожиданиям, но может быть полезно для некоторых целей. Если Стандарт запрещает необычное поведение, качественные реализации, предназначенные для целей, которые могут получить от него значительную выгоду, должны поддерживать как соответствующий режим, который ведет себя обычно ожидаемым образом, так и несоответствующий режим, который ведет себя необычным образом, но даже если Стандарт разрешает реализации поведения, если еще предлагает те же два режима.
К сожалению, есть различные случаи, когда компиляторы этого не делают, особенно когда они начинают встраивать материал, вы можете оказаться, что вычисления будут выполнены с результатами в расширенных регистрах с плавающей запятой, которые даже превышают длинную двойную точность (обычно на один бит) . Переработка изменчивости оказалась для меня единственной надежной вещью.
@PlasmaHH: Покажите нам пример кода с реализацией, которая не устраняет избыточную точность, когда приведение или присваивание выполняется вместе со сборкой, созданной этой реализацией.
Независимо от того, разрешено ли такое приведение быть оптимизированным, это действительно случается и временное назначение останавливает его.
Например, компиляция MSVC для 32-битной версии (с использованием x87) с /Ox /fp:fast:
_x$ = 8 ; size = 8
float uselessCast(double) PROC ; uselessCast
fld QWORD PTR _x$[esp-4]
ret 0
float uselessCast(double) ENDP ; uselessCast
_y$ = 8 ; size = 4
_x$ = 8 ; size = 8
float coerceToFloat(double) PROC ; coerceToFloat
fld QWORD PTR _x$[esp-4]
fstp DWORD PTR _y$[esp-4]
fld DWORD PTR _y$[esp-4]
ret 0
float coerceToFloat(double) ENDP
Где uselessCast, как показано ниже, а coerceToFloat, как в вопросе.
float uselessCast(double x)
{
return static_cast<float>(x);
}
Аналогично GCC и Clang с -O3 -ffast-math -m32 -mfpmath=387
uselessCast(double):
fld QWORD PTR [esp+4]
ret
coerceToFloat(double):
sub esp, 20
fld QWORD PTR [esp+24]
fstp DWORD PTR [esp+12]
fld DWORD PTR [esp+12]
add esp, 20
ret
Ссылка Godbolt для всего вышеперечисленного
Конечно, вы можете возразить, что с /fp:fast или -ffast-math вы в любом случае не должны ожидать чего-либо от арифметики с плавающей запятой, но она вам может понадобиться, но при этом вы сможете отбросить лишнюю точность.
Я понял, что полезно разрешить программистам разрешить компиляторам отказываться от усечения / округления при сохранении значений в автоматических объектах, соответствующих регистру. Это будет верно как для небольших целочисленных типов со знаком, так и для типов с плавающей запятой (так что после register int8_t foo=127; foo = foo+1; значение foo может хранить либо +128, либо -128 в свободное время компилятора). Обратите внимание, что такое присвоение не UB. Однако способность программиста использовать такие исключения будет подорвана из-за того, что компилятор также применяет их к таким вещам, как приведение типов, единственной целью которых могло быть принудительное усечение.
Нет никакой разницы. Что касается причин, то на самом деле это не то, о чем мы можем размышлять (особенно без дополнительного контекста). Вы должны спросить об этом у первоначального автора.