«Почему» в том, что некоторые компиляторы возвращают значения с плавающей запятой в регистре с плавающей запятой. Эти регистры имеют только один размер. Например, в X86 его ширина составляет 80 бит. Результаты функции, которая возвращает значение с плавающей запятой, будут помещены в этот регистр независимо от того, был ли тип объявлен как float, double, float_t или double_t. Если размер возвращаемого значения и размер регистра с плавающей запятой различаются, то в какой-то момент потребуется инструкция для округления до желаемого размера.
Такой же тип преобразования необходим и для целых чисел, но для последующих сложений и вычитаний нет накладных расходов, потому что есть инструкции, чтобы выбрать, какие байты включить в операцию. Правила для преобразования целых чисел в меньший размер указывают, что отбрасываются старшие значащие биты, поэтому результат уменьшения может привести к радикально другому результату (например, (короткий) (2147450880) -> -32768), но для какая-то причина, которая, кажется, в порядке с сообществом программистов.
При выполнении уменьшения чисел с плавающей точкой результат указывается округленным до ближайшего представимого числа. Если бы целые числа подчинялись одним и тем же правилам, то приведенный выше пример усек бы таким образом (short) (2147450880) -> +32767. Очевидно, что для выполнения такой операции требуется немного больше логики, чем простое усечение старших битов. С плавающей точкой, экспонента и значение и изменяют размеры между плавающим, двойным и длинным двойным, так что это сложнее. Кроме того, существуют проблемы преобразования между бесконечностью, 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) и иметь разные ветви. Третий метод - использовать какой-нибудь эпсилон, который будет использоваться для проверки того, достаточно ли близки результаты.
К сожалению, есть некоторые вычисления, которые принимают решение на основе результатов вычислений, что приводит к true или false , который не может быть решен с помощью эпсилона. Это происходит в компьютерной графике, например, чтобы решить, находится ли точка внутри или снаружи многоугольника, который определяет, должен ли быть заполнен определенный пиксель. Если ваша регрессия включает один из них, вы не можете использовать метод epsilon и должны использовать разные ветви в зависимости от вычислительной модели.
Еще один способ разрешить регрессию решений между моделями - явное приведение результата к определенной желаемой точности. Это работает в большинстве случаев на многих компиляторах, но некоторые компиляторы считают, что они умнее вас, и отказываются выполнять преобразование. Это происходит в случае, когда промежуточный результат сохраняется в регистре, но используется в последующих вычислениях. В промежуточном результате вы можете отбрасывать столько точности, сколько хотите, но компилятор ничего не сделает - , если вы не объявите промежуточный результат как volatile . Это затем вынуждает компилятор уменьшать размер и сохранять промежуточный результат в переменной указанного размера в памяти , а затем извлекать его при необходимости для вычислений. Стандарт IEEE с плавающей запятой точный для элементарных операций (+ - * /) и квадратного корня. Я полагаю, что sin (), cos (), exp (), log () и т. Д. Указаны в пределах 2 ULP (единицы в наименьшей значимой позиции) от ближайшего числово представимого результата. Длинный двойной (80 бит) формат был разработан, чтобы позволить вычисление этих других трансцендентных функций точно до ближайшего числово представимого результата.
Это охватывает многие проблемы, поднятые (и подразумеваемые) в этой теме, но не отвечает на вопрос, когда вам следует использовать типы float_t и double_t. Очевидно, это необходимо сделать при взаимодействии с API, который использует эти типы, особенно при передаче адреса одного из этих типов.
Если вас больше всего беспокоит производительность, то вы можете рассмотреть возможность использования типов float_t и double_t в своих вычислениях и API. Но наиболее вероятно, что увеличение производительности, которое вы получите, не будет ни измеримым, ни заметным.
Однако, если вас беспокоит регрессия между разными компиляторами и разными машинами, вам, вероятно, следует избегать этих типов в максимально возможной степени и свободно использовать приведение типов для обеспечения кросс-платформенной совместимости.