Какая точка float_t и когда она должна использоваться?

Я работаю с клиентом, который использует старую версию GCC (3.2.3, если быть точным), но хочет обновить, и одна из причин, которые были даны как камнем преткновения для обновления до более новой версии, - это различия в размере типа 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 или double_t?

Ответ 1

Причиной для float_t является то, что для некоторых процессоров и компиляторов используется более крупный тип, например. long double for float может быть более эффективным, и поэтому float_t позволяет компилятору использовать более крупный тип вместо float.

Таким образом, в случае OPs, использующем float_t, изменение размера соответствует стандарту. Если исходный код хотел использовать меньшие размеры поплавка, он должен использовать float.

Существует некоторое обоснование в open-std doc

например, определения типов float_t и double_t (определенные в < math.h > ), предназначены для эффективное использование архитектур с более эффективные, более широкие форматы. Приложения

Ответ 2

"Почему" заключается в том, что некоторые компиляторы вернут значения с плавающей запятой в регистр с плавающей запятой. Эти регистры имеют только один размер. Например, на X86 он имеет ширину 80 бит. Результаты функции, возвращающей значение с плавающей запятой, будут помещены в этот регистр независимо от того, был ли тип объявлен как float, double, float_t или double_t. Если размер возвращаемого значения и размер регистра с плавающей запятой различаются, тогда в какой-то момент потребуется команда для округления до нужного размера.

То же самое преобразование необходимо и для целых чисел, но для последующих добавлений и вычитаний нет накладных расходов, потому что есть инструкции для выбора, какие байты включать в операцию. Правила преобразования целых чисел в меньший размер указывают, что наиболее значимые биты будут отброшены, поэтому результат сокращения может привести к радикальному изменению результата (например, (короткий) (2147450880) → -32768), но для по какой-то причине, похоже, что с сообществом разработчиков все в порядке.

При выполнении сокращения с плавающей запятой результат определяется округленным до ближайшего числа. Если целые числа были подчинены тем же правилам, то приведенный выше пример будет обрезать (коротко) (2147450880) → +32767. Очевидно, для выполнения такой операции требуется немного больше логики, которая просто усекает верхние биты. С плавающей точкой, экспонентой и значимыми изменениями размеров между float, double и long double, так что это сложнее. Кроме того, существуют проблемы конверсии между бесконечностью, NaN, нормализованными числами и перенормированными числами, которые необходимо учитывать. Аппаратное обеспечение может реализовать эти преобразования за тот же период времени, что и целочисленное добавление, но если преобразование должно быть реализовано в программном обеспечении, может потребоваться 20 инструкций, которые могут оказать заметное влияние на производительность. Поскольку модель программирования C гарантирует, что одни и те же результаты будут созданы независимо от того, реализована ли плавающая запятая в аппаратном или программном обеспечении, программное обеспечение обязано выполнить эти дополнительные инструкции, чтобы соответствовать вычислительной модели. Типы float_t и double_t были разработаны для отображения наиболее эффективного типа возвращаемого значения.

Компилятор определяет FLT_EVAL_METHOD, который указывает, насколько точность должна использоваться в промежуточных вычислениях. При использовании целых чисел правило состоит в том, чтобы выполнять промежуточные вычисления с использованием максимальной точности используемых операндов. Это будет соответствовать FLT_EVAL_METHOD == 0. Однако исходный K & R указал, что все промежуточные вычисления выполняются в двойном режиме, что дает FLT_EVAL_METHOD == 1. Однако с внедрением стандарта IEEE с плавающей запятой он стал обычным явлением на некоторых платформах, в частности, Macintosh PowerPC и Windows X86 для выполнения промежуточных вычислений в длинных двоичных бит - 80 бит, что дает FLT_EVAL_METHOD= = 2.

Регрессионное тестирование будет зависеть от вычислительной модели FLT_EVAL_METHOD. Таким образом, ваш регрессионный код должен учитывать это. Один из способов - проверить FLT_EVAL_METHOD и иметь разные ветки для каждой модели. Аналогичным методом будет проверка sizeof (float_t) и наличие разных ветвей. Третий метод - использовать какой-то эпсилон, который будет использоваться, чтобы проверить, достаточно ли близки результаты.

К сожалению, есть некоторые вычисления, которые принимают решение на основе результатов вычисления, приводящее к истинному или ложному, которое не может быть разрешено с помощью epsilon. Это происходит в компьютерной графике, например, чтобы решить, находится ли точка внутри или вне многоугольника, которая определяет, должен ли быть заполнен конкретный пиксель. Если ваша регрессия включает один из них, вы не можете использовать метод epsilon и должны использовать разные ветки в зависимости от вычислительной модели.

Еще один способ разрешить регрессию решения между моделями - явно указать результат на определенную желаемую точность. Это работает большую часть времени на многих компиляторах, но некоторые компиляторы считают, что они умнее вас, и отказываются выполнять преобразование. Это происходит в случае, когда промежуточный результат сохраняется в регистре, но используется при последующем вычислении. Вы можете отбросить точность столько, сколько хотите в промежуточном результате, но компилятор ничего не сделает - если вы не объявите промежуточный результат как volatile. Затем это заставляет компилятор уменьшать размер и хранить промежуточный результат в переменной указанного размера в памяти, а затем извлекать его, когда это необходимо для вычисления. Стандарт с плавающей точкой IEEE является точным для элементарных операций (+ - */) и квадратного корня. Я считаю, что sin(), cos(), exp(), log() и т.д. Указаны в пределах 2 ULP (единицы в наименее значимом положении) ближайшего численно-представимого результата. Длинный двойной (80-битный) формат был разработан таким образом, чтобы вычислять эти другие трансцендентные функции в точности до ближайшего результата с численным представлением.

Это охватывает множество проблем, возникающих (и подразумеваемых) в этом потоке, но не отвечает на вопрос о том, когда вы должны использовать типы float_t и double_t. Очевидно, что вам нужно сделать это при взаимодействии с API, который использует эти типы, особенно при передаче адреса одного из этих типов.

Если ваша главная проблема связана с производительностью, вы можете захотеть использовать типы float_t и double_t в своих вычислениях и API. Но наиболее вероятно, что увеличение производительности, которое вы получаете, не является ни измеримым, ни заметным.

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

Ответ 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 по умолчанию.