Значение фразы «Компилятор AC по умолчанию сохраняет константу с плавающей запятой как двойную»

Я только начинал изучать 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?

Настоящий компилятор godbolt.org/z/eYfsEKf8P заботится только о типе переменной, константа 5/5.0/5.0f создает здесь тот же код. Текст может быть устаревшим/неточным/не следует воспринимать буквально. Употреблять с солью.

teapot418 08.07.2024 14:24

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

Tomasz Kalisiak 08.07.2024 14:26

Это часть, «конвертируемая при необходимости». Чтобы сохранить значение в x, компилятор преобразует его в float.

BoP 08.07.2024 14:34
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
4
86
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Утверждение автора о том, что «По умолчанию плавающие константы хранятся как числа двойной точности», вероятно, вытекает из этого абзаца стандарта 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, умножить на константу double0.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.

Другие вопросы по теме