Я только начинал изучать C (программирование на C К.Н. Кинга), когда наткнулся на следующий отрывок:
По умолчанию плавающие константы хранятся как числа двойной точности. В других Другими словами, когда компилятор C находит константу
57.0
в программе, он подготавливает число, которое будет храниться в памяти в том же формате, что и переменнаяdouble
. Этот Правило обычно не вызывает проблем, поскольку значенияdouble
преобразуются автоматически наfloat
при необходимости.
Предположим, у меня есть следующие утверждения:
float x = 5.0; // 1
float y = 5.0f; // 2
Что означает этот отрывок в этом примере? В чем разница между утверждениями 1 и 2 относительно хранения значений в битах?
В первом утверждении 5.0
сначала сохраняется как double
, а затем выделяется как float
для x
?
Если вы сразу присвоите его переменной с плавающей запятой, компилятор фактически сохранит ее как число с плавающей запятой в вашем исполняемом файле в качестве оптимизации. Это называется «постоянное складывание». Компилятор заранее знает результат преобразования, поэтому вашей программе не нужно вычислять его во время выполнения.
Это часть, «конвертируемая при необходимости». Чтобы сохранить значение в x
, компилятор преобразует его в float
.
Аналогично Когда добавление буквы «f» меняет значение плавающей константы, присвоенной ей с плавающей запятой?`
Утверждение автора о том, что «По умолчанию плавающие константы хранятся как числа двойной точности», вероятно, вытекает из этого абзаца стандарта C, C 2018 6.4.4.2 4:
Плавающая константа без суффикса имеет тип
double
. Если суффикс состоит из буквыf
илиF
, он имеет типfloat
. Если суффикс состоит из буквыl
илиL
, он имеет типlong double
.
Этот абзац ясно дает понять, что константа с плавающей запятой в исходном коде по умолчанию (то есть не имеет суффикса) интерпретируется как double
. Но утверждение автора о том, что ценность «хранится», неточно. Стандарт C говорит нам, как интерпретировать исходный код, но не требует сохранения констант. Даже в абстрактной модели машины, которую стандарт C использует для определения семантики C, перед рассмотрением оптимизации указывается только то, что значения переменных хранятся в памяти переменных, а не то, что значения констант сохраняются.
Таким образом, я ожидаю, что компилятор сделает все возможное, чтобы преобразовать константу без суффикса в double
,1, но я не обязательно ожидаю, что он будет хранить ее где-либо, кроме своей собственной памяти, во время работы с ней и генерации программы. При необходимости он может сохранить его в данных программы, но может сгенерировать его в инструкциях или сложить в другие части выражения.
Это правило обычно не вызывает проблем, поскольку при необходимости значения типа double автоматически преобразуются в числа с плавающей запятой.
Я бы сформулировал это так: проблемы, вызванные этим автоматическим преобразованием, редки. Заявление о том, что это «в целом» не вызывает проблем, может привести к тому, что учащийся воспримет это как общее правило, а не будет осторожно относиться к тому, когда могут возникнуть проблемы. В ситуациях, когда константы с плавающей запятой тщательно разрабатываются для конкретной задачи, следует использовать суффиксы, чтобы гарантировать, что константа имеет именно желаемое значение.
В вашем примере с пятью float x = 5.0;
и float y = 5.0f;
дадут одно и то же значение в x
, потому что пять можно представить как в float
, так и в double
. Однако рассмотрим этот код:
#include <stdio.h>
int main(void)
{
float x = 0x9.876548000000000000001p0;
float y = 0x9.876548000000000000001p0f;
printf("%a\n", x);
printf("%a\n", y);
}
В моей реализации на C x
и y
получают разные значения, и это печатает:
0x1.30eca8p+3 0x1.30ecaap+3
Причина в следующем:
В float x = 0x9.876548000000000000001p0;
9.87654800000000000000116 преобразуется в double
. Последний 1 бит на несколько бит ниже того, что можно представить в виде двойного числа, поэтому он округляется в меньшую сторону и дает 9,87654816. Затем этот double
преобразуется в float
для хранения в x
. Младший бит числа 4 — это последний бит, который помещается в float
, поэтому первый бит числа 8 — это первый бит, который не подходит. Это находится на полпути между двумя значениями, представленными в виде float
, 9,8765416 и 9,8765516. В случае равенства правилом является округление до четного младшего бита, поэтому результат преобразования равен 9,8765416 и сохраняется в x
. При его печати получается 0x1.30eca8p+3
, что является еще одним представлением этого числа.
В float y = 0x9.876548000000000000001p0f;
9.87654800000000000000116 преобразуется в float
. Опять же, младший бит из 4 подходит, поэтому неподходящая часть - это 8000000000000001. Из-за 1 это больше половины пути от 9,8765416 до 9,8765516, поэтому связи нет, и округление до ближайшего значения выдает 9,8765516, и это то, что хранится в y
. При его печати получается 0x1.30ecaap+3
, другое представление того же значения.
1 Стандарт C небрежен в отношении преобразования констант с плавающей запятой в значения с плавающей запятой. C 2018 6.4.4.2 7 говорит, что преобразование времени перевода «должно» соответствовать преобразованию, выполняемому библиотечными функциями, такими как strtod
, а 7.22.1.3 9 говорит, что strtod
«должно» быть правильно округлено, если в нем не слишком много цифр (максимум DECIMAL_DIG
цифры) или, если да, должно равняться результату преобразования одного из двух десятичных чисел с DECIMAL_DIG
цифрами, которые непосредственно связывают значение. Это наследие из-за того, что преобразование значений с показателями степени, такими как +300
или -300
, с нуля номинально требует вычислений с сотнями цифр, что считалось слишком большим бременем для ранних компиляторов и компьютеров. Для этого разработаны современные алгоритмы, поэтому стандарт может требовать правильного округления во всех случаях.
В некоторых случаях неявное преобразование может неожиданно повлиять на производительность. Например, если у вас есть простая функция вроде:
float foo(float x){
return 0.1 * x;
}
Компилятору придется сначала преобразовать x
из float
в double
, умножить на константу double
0.1
, затем преобразовать результат обратно в float
, т. е. потребуется 3 операции вместо одного умножения с одинарной точностью, если вы написали:
return 0.1f * x;
Если у вас всего несколько 1000 операций с плавающей запятой в секунду, влияние этого, скорее всего, незначительно, но в этом случае вы также можете использовать переменные double
везде (на современном оборудовании не должно быть разницы в производительности между одним float
или double
умножение/сложение).
Я предлагаю рассмотреть возможность использования float
только в том случае, если вам нужно хранить много значений или обрабатывать большое количество значений за короткое время, и вам не нужна дополнительная точность double
(то, что означает «много», очень зависит от конкретной проблемы). ).
Н.Б. В gcc и clang есть флаг -Wconversion
, который предупредит вас о многих случаях, когда неявное преобразование может потерять точность.
float x = 5.0; // no precision loss, no warning
float y = 0.1; // warning: conversion from 'double' to 'float' changes value from '1.0000000000000001e-1' to '1.00000001e-1f' [-Wfloat-conversion] (gcc 14.1)
float z = 0.1f; // no conversion, no warning.
Настоящий компилятор godbolt.org/z/eYfsEKf8P заботится только о типе переменной, константа 5/5.0/5.0f создает здесь тот же код. Текст может быть устаревшим/неточным/не следует воспринимать буквально. Употреблять с солью.