#c #gcc #floating-point #double
#c #gcc #значение с плавающей запятой #двойной
Вопрос:
Я работаю с клиентом, который использует старую версию GCC (3.2.3, если быть точным), но хочет обновиться, и одной из причин, которая была указана в качестве камня преткновения при обновлении до более новой версии, являются различия в размере type float_t
, что, конечно же, правильно:
В GCC 3.2.3
sizeof(float_t) = 12
sizeof(float) = 4
sizeof(double_t) = 12
sizeof(double) = 8
В GCC 4.1.2
sizeof(float_t) = 4
sizeof(float) = 4
sizeof(double_t) = 8
sizeof(double) = 8
но в чем причина этого различия? Почему размер стал меньше, и когда вы должны и не должны использовать float_t
or double_t
?
Комментарии:
1. Старый GCC, вероятно, просто использовал typedef для
long double
.float_t
иdouble_t
— это C99 (который не сильно поддерживался старыми версиями GCC).2. @Let_Me_Be Разве sizeof (long double) не равен 16?
3. @LumpN зависит от платформы. Определенно может быть, но почему это должно быть?
Ответ №1:
Причина float_t в том, что для некоторых процессоров и компиляторов использование большего типа, например, long double для float, может быть более эффективным, и поэтому float_t позволяет компилятору использовать больший тип вместо float .
таким образом, в случае операций с использованием float_t изменение размера — это то, что допускает стандарт. Если исходный код хотел использовать меньшие размеры float, он должен использовать float.
В документе open-std есть некоторое обоснование
например, определения типов float_t и double_t (определенные в <math.h>) предназначены для обеспечения эффективного использования архитектур с более эффективными и широкими форматами. Приложения
Комментарии:
1. Это имеет смысл. Теперь я понимаю, почему это становится важным при регрессионном тестировании перенесенных приложений: числа, которые должны быть одинаковыми, могут отличаться со значительным отрывом без какой-либо очевидной причины. Таким образом, предполагаемая проблема, с которой я сталкиваюсь.
2. Я не думаю, что использование
float_t
вместоfloat
(илиdouble_t
вместоdouble
) всегда будет повышать эффективность. Если вы это сделаете, то компилятор будет вынужден сохранять все промежуточные результаты, включая утечки регистров, с двойной точностью. Наиболее эффективный код для обеспечения точности, требуемой вашей программой, скорее всего, будет включать в себя сочетание хранилища двойной и расширенной точности.3. Какие размеры имеют эти типы на разных платформах? Зависит ли это от процессора, операционной системы и / или компилятора? Почему разные версии GCC выдают разные результаты для OP?
Ответ №2:
«Почему» заключается в том, что некоторые компиляторы возвращают значения с плавающей запятой в регистре с плавающей запятой. Эти регистры имеют только один размер. Например, на X86 его ширина составляет 80 бит. Результаты функции, возвращающей значение с плавающей запятой, будут помещены в этот регистр независимо от того, был ли тип объявлен как float, double, float_t или double_t. Если размер возвращаемого значения и размер регистра с плавающей запятой отличаются, то в какой-то момент потребуется инструкция для округления в меньшую сторону до желаемого размера.
Такой же тип преобразования необходим и для целых чисел, но для последующих сложений и вычитаний нет накладных расходов, потому что есть инструкции по выбору того, какие байты задействовать в операции. Правила преобразования целых чисел в меньший размер определяют, что наиболее значимые биты отбрасываются, поэтому результат уменьшения размера может привести к результату, который радикально отличается (например, (short) (2147450880) —> -32768), но по какой-то причине это, похоже, устраивает сообщество программистов.
При уменьшении размера с плавающей запятой результат указывается округленным до ближайшего представимого числа. Если бы целые числа подчинялись тем же правилам, то приведенный выше пример был бы усечен таким образом (short) (2147450880) -> 32767. Очевидно, что для выполнения такой операции требуется немного больше логики, чем простое усечение старших битов. С плавающей запятой показатель степени и значение изменяют размеры между float, double и long double, поэтому это сложнее. Кроме того, существуют проблемы преобразования между infinity, NaN, нормализованными числами и перенормированными числами, которые необходимо учитывать. Аппаратное обеспечение может реализовать эти преобразования за то же время, что и сложение целых чисел, но если преобразование необходимо реализовать программно, может потребоваться 20 инструкций, что может оказать заметное влияние на производительность. Поскольку модель программирования на C гарантирует получение одинаковых результатов независимо от того, реализована ли функция с плавающей запятой аппаратно или программно, программное обеспечение обязано выполнять эти дополнительные инструкции, чтобы соответствовать вычислительной модели. Типы float_t и double_t были разработаны для предоставления наиболее эффективного типа возвращаемого значения.
Компилятор определяет FLT_EVAL_METHOD, который определяет, какая точность должна использоваться в промежуточных вычислениях. При работе с целыми числами правило заключается в выполнении промежуточных вычислений с использованием наивысшей точности задействованных операндов. Это соответствовало бы FLT_EVAL_METHOD==0. Однако в исходном K amp; R указано, что все промежуточные вычисления выполняются в double, что приводит к FLT_EVAL_METHOD==1. Однако с введением стандарта IEEE с плавающей запятой на некоторых платформах, в частности Macintosh PowerPC и Windows X86, стало обычным делом выполнять промежуточные вычисления в длинных двойных — 80 битах, что дает FLT_EVAL_METHOD==2.
На регрессионное тестирование будет влиять вычислительная модель FLT_EVAL_METHOD. Таким образом, ваш регрессионный код должен учитывать это. Один из способов — протестировать FLT_EVAL_METHOD и иметь разные ветви для каждой модели. Аналогичным методом было бы протестировать sizeof(float_t) и иметь разные ветви. Третьим методом было бы использование некоторого вида epsilon, который использовался бы для проверки, достаточно ли близки результаты.
К сожалению, существуют некоторые вычисления, которые принимают решение на основе результатов вычисления, в результате чего получается true или false, которые не могут быть разрешены с помощью epsilon. Это происходит в компьютерной графике, например, чтобы решить, находится ли точка внутри или снаружи многоугольника, что определяет, следует ли заполнять конкретный пиксель. Если ваша регрессия связана с одним из этих способов, вы не можете использовать метод epsilon и должны использовать разные ветви в зависимости от вычислительной модели.
Другой способ разрешить регрессию принятия решений между моделями — явно привести результат к определенной желаемой точности. В большинстве случаев это работает на многих компиляторах, но некоторые компиляторы считают, что они умнее вас, и отказываются выполнять преобразование. Это происходит в случае, когда промежуточный результат сохраняется в регистре, но используется в последующих вычислениях. Вы можете сколько угодно отбрасывать точность в промежуточном результате, но компилятор ничего не сделает — если вы не объявите промежуточный результат как изменчивый. Затем это вынуждает компилятор уменьшить размер и сохранить промежуточный результат в переменной указанного размера в памяти, чтобы затем извлечь его, когда это необходимо для вычисления. Стандарт IEEE с плавающей запятой точен для элементарных операций ( -*/) и квадратного корня. Я полагаю, что sin(), cos(), exp(), log() и т.д. Указаны в пределах 2 ULP (единиц измерения в наименее значимой позиции) от ближайшего численно представимого результата. Формат long double (80 бит) был разработан для того, чтобы позволить вычислять эти другие трансцендентные функции с точностью до ближайшего численно представимого результата.
Это охватывает множество проблем, поднятых (и подразумеваемых) в этой теме, но не отвечает на вопрос о том, когда вам следует использовать типы float_t и double_t. Очевидно, что это необходимо делать при взаимодействии с API, который использует эти типы, особенно при передаче адреса одного из этих типов.
Если вас больше всего беспокоит производительность, то вы можете рассмотреть возможность использования типов float_t и double_t в ваших вычислениях и API. Но наиболее вероятно, что получаемое вами увеличение производительности не поддается измерению и не заметно.
Однако, если вас беспокоит регрессия между разными компиляторами и разными машинами, вам, вероятно, следует избегать этих типов, насколько это возможно, и широко использовать приведение для обеспечения кроссплатформенной совместимости.
Комментарии:
1. Предположим, что обычно все регистры с плавающей запятой в данном FPU имеют только один размер, но это не является обязательным требованием, чтобы они это делали.
Ответ №3:
Стандарт C99 гласит:
Типы
float_t
double_t
являются ли плавающие типы по крайней мере такими же широкими, как float и double соответственно, и такими,
double_t
что по крайней мере такими же широкими, какfloat_t
. ЕслиFLT_EVAL_METHOD
равно0
, тоfloat_t
иdouble_t
являютсяfloat
иdouble
соответственно; еслиFLT_EVAL_METHOD
равно1
, то они обаdouble
; еслиFLT_EVAL_METHOD
равно2
, то они обаlong double
; а для других значенийFLT_EVAL_METHOD
они в противном случае определяются реализацией.178)
И действительно, в предыдущих версиях gcc они были определены как long double
по умолчанию.
Комментарии:
1. Это «что», но не «почему». Я не вижу смысла в использовании этих типов в отличие от более привычных
float
иdouble
. Я уверен, что есть веская причина, я просто не уверен, какая именно.2. Что ж, эти типы позволяют вам сделать оба типа одинаковыми с помощью
#define FLT_EVAL_METHOD 1
, и поэтому вы можете удалить некоторыеfloat -> double
преобразования, если это критично для вашего кода. Кроме этого, я также не нашел ему никакого применения.3. Почему GCC изменил настройку по умолчанию?