В чем смысл float_t и когда его следует использовать?

#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 изменил настройку по умолчанию?