Должны ли мы обычно использовать float-литералы для поплавков вместо простых двойных литералов?
В С++ (или, может быть, только наши компиляторы VC8 и VC10)3.14
- это двойной литерал, а 3.14f
- это плавающий литерал.
Теперь у меня есть коллега, который заявил:
Мы должны использовать float-литералы для вычислений float и двухлитровых для двойных вычислений, поскольку это может повлиять на точность вычисления, когда константы используются в вычислении.
В частности, я думаю, он имел в виду:
double d1, d2;
float f1, f2;
... init and stuff ...
f1 = 3.1415 * f2;
f1 = 3.1415f * f2; // any difference?
d1 = 3.1415 * d2;
d1 = 3.1415f * d2; // any difference?
Или, добавленный мной, даже:
d1 = 42 * d2;
d1 = 42.0f * d2; // any difference?
d1 = 42.0 * d2; // any difference?
В общем, единственная точка, которую я могу увидеть при использовании 2.71828183f
, - это убедиться, что константа, которую я пытаюсь указать, действительно будет вписываться в float (иначе компилятор/предупреждение).
Может кто-то пролить свет на это? Вы указываете постфикс f
? Почему?
Чтобы процитировать ответ, я неявно принял это как должное:
Если вы работаете с переменной float и двойным литеральным операция будет выполняться как двойная, а затем конвертируется обратно в float.
Может ли быть какой-либо вред в этом? (За исключением очень, очень теоретического воздействия на производительность?)
Дальнейшее редактирование: Было бы неплохо, если бы ответы, содержащие технические детали (оцененные!), также могли бы включать в себя то, как эти различия влияют на код общего назначения. (Да, если вы хрустали число, вы, вероятно, хотели бы убедиться, что ваши операции с большими числами с плавающей запятой максимально эффективны (и правильны), насколько это возможно, но имеет ли значение код общего назначения, который называется несколько раз? Isn ' t чище, если код просто использует 0.0
и пропускает суффикс с плавающей точкой для поддержания! - float?)
Ответы
Ответ 1
Да, вы должны использовать суффикс f
. Причины включают:
- Производительность
. Когда вы пишете float foo(float x) { return x*3.14; }
, вы вынуждаете компилятор испускать код, который преобразует x в double, затем выполняет умножение, а затем возвращает результат в одиночный. Если вы добавите суффикс f
, то оба преобразования будут устранены. На многих платформах каждый из этих преобразований примерно такой же дорогой, как и само умножение.
-
Производительность (продолжение). Существуют платформы (например, большинство мобильных телефонов), на которых арифметика с двойной точностью значительно медленнее, чем с одной точностью. Даже игнорируя накладные расходы на конвертацию (см. 1.), каждый раз, когда вы вынуждаете вычисление оцениваться в два раза, вы замедляете свою программу. Это не просто "теоретическая" проблема.
-
Уменьшите подверженность ошибкам. Рассмотрим пример float x = 1.2; if (x == 1.2) // something;
Is something
? Нет, это не так, потому что x удерживает 1.2
округленным до float
, но сравнивается с значением двойной точности 1.2
. Эти два не равны.
Ответ 2
Я подозреваю что-то вроде этого: если вы работаете с переменной float и двойным литералом, вся операция будет выполняться как двойная, а затем конвертируется обратно в float.
Если вы используете литерал с плавающей точкой, на самом деле вычисление будет выполняться с точностью до float, хотя некоторое аппаратное обеспечение будет преобразовывать его в двойное, чтобы выполнить вычисления.
Ответ 3
Я сделал тест.
Я скомпилировал этот код:
float f1(float x) { return x*3.14; }
float f2(float x) { return x*3.14F; }
Использование gcc 4.5.1 для i686 с оптимизацией -O2.
Это был код сборки, сгенерированный для f1:
pushl %ebp
movl %esp, %ebp
subl $4, %esp # Allocate 4 bytes on the stack
fldl .LC0 # Load a double-precision floating point constant
fmuls 8(%ebp) # Multiply by parameter
fstps -4(%ebp) # Store single-precision result on the stack
flds -4(%ebp) # Load single-precision result from the stack
leave
ret
И это код сборки, сгенерированный для f2:
pushl %ebp
flds .LC2 # Load a single-precision floating point constant
movl %esp, %ebp
fmuls 8(%ebp) # Multiply by parameter
popl %ebp
ret
Итак, интересно то, что для f1 компилятор сохранил значение и снова загрузил его, чтобы убедиться, что результат был усечен до одной точности.
Если мы используем опцию -ffast-math, то это различие значительно уменьшается:
pushl %ebp
fldl .LC0 # Load double-precision constant
movl %esp, %ebp
fmuls 8(%ebp) # multiply by parameter
popl %ebp
ret
pushl %ebp
flds .LC2 # Load single-precision constant
movl %esp, %ebp
fmuls 8(%ebp) # multiply by parameter
popl %ebp
ret
Но по-прежнему существует разница между загрузкой одной или двойной точности.
Обновление для 64-разрядных
Это результаты с gcc 5.2.1 для x86-64 с оптимизацией -O2:
f1:
cvtss2sd %xmm0, %xmm0 # Convert arg to double precision
mulsd .LC0(%rip), %xmm0 # Double-precision multiply
cvtsd2ss %xmm0, %xmm0 # Convert to single-precision
ret
f2:
mulss .LC2(%rip), %xmm0 # Single-precision multiply
ret
С -ffast-math результаты одинаковы.
Ответ 4
Как правило, я не думаю, что это будет иметь значение, но это стоит
указывая, что 3.1415f
и 3.1415
(обычно) не равны. На
с другой стороны, вы обычно не делаете никаких вычислений в float
во всяком случае, по крайней мере на обычных платформах. (double
так же быстро, если
не быстрее.) О единственном времени, когда вы должны увидеть float
, - это когда
являются большими массивами, и даже тогда все вычисления будут обычно
сделайте в double
.
Ответ 5
Есть разница: если вы используете двойную константу и умножаете ее на переменную float, переменная сначала преобразуется в double, вычисление выполняется в double, а затем результат преобразуется в float. Хотя точность здесь не является проблемой, это может привести к путанице.
Ответ 6
Я лично склонен использовать нотацию f postfix в качестве основы и сделать ее очевидной, насколько могу, это тип float, а не двойной.
Мои два цента
Ответ 7
Из С++ Standard (рабочий проект), раздел 5 о бинарных операторах
Многие двоичные операторы, которые ожидают операнды арифметики или тип перечисления вызывает преобразования и дает результаты в аналогичных путь. Цель состоит в том, чтобы дать общий тип, который также является типом результат. Этот шаблон называется обычным арифметическим преобразованием, которые определяются следующим образом: - Если либо операнд имеет область действия тип перечисления (7.2), конверсии не выполняются; если другой операнд не имеет одного и того же типа, выражение плохо сформировано. - Если любой из операндов имеет тип long double, другой должен быть преобразован длинный двойной. - В противном случае, если один операнд двойной, другой должны быть преобразованы в двойные. - В противном случае, если один из операндов плавает, другой должен быть преобразован в float.
А также раздел 4.8
Значение отклоняющегося типа точки может быть преобразовано в prvalue другой тип точечного типа. Если исходное значение может быть точно представленный в типе адресата, результатом преобразования является это точное представление. Если значение источника находится между двумя соседними целевые значения, результатом преобразования является реализация - определенный выбор любой из этих ценностей. В противном случае поведение не определено
В результате этого вы можете избежать ненужных преобразований, указав свои константы в точности, заданной типом назначения, при условии, что вы не потеряете точность вычисления при этом (т.е. ваши операнды точно представлены в точность целевого типа).