Должны ли мы вообще использовать литералы с плавающей точкой для чисел вместо простых двойных литералов? - PullRequest
28 голосов
/ 05 октября 2011

В C ++ (или, может быть, только наши компиляторы VC8 и VC10) 3.14 - это двойной литерал, а 3.14f - это литерал с плавающей точкой.

Теперь у меня есть коллега, который заявил:

Мы должны использовать литералы с плавающей запятой для вычислений с плавающей запятой и двойные литералы для двойных вычислений, поскольку это может повлиять на точность вычисления при использовании констант в вычислении.

В частности, я думаю, что он имел в виду:

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 заключается в том, чтобы убедиться, что константа, которую я пытаюсь указать, действительно поместится в число с плавающей точкой (в противном случае ошибка / предупреждение компилятора).

Может кто-нибудь пролить свет на это?Вы указываете постфикс f?Почему?

Чтобы процитировать ответ, который я неявно воспринимал как должное:

Если вы работаете с переменной с плавающей точкой и двойным литералом, вся операция будет выполнена как двойнаяи затем преобразуется обратно в плавающее.

Может ли быть в этом какой-нибудь вред?(Кроме очень, очень теоретического влияния на производительность?)

Дальнейшее редактирование: Было бы неплохо, если бы ответы, содержащие технические детали (приветствуются!), Могли также включать, как эти различия влияюткод общего назначения .(Да, если вы работаете с числами, вам, вероятно, хотелось бы убедиться, что ваши операции с плавающей запятой с большим числом n настолько эффективны (и корректны), насколько это возможно - но имеет ли это значение для кода общего назначения, который вызывается несколько раз?Это чище, если код просто использует 0.0 и пропускает - трудно поддерживать! - суффикс float?)

Ответы [ 7 ]

48 голосов
/ 05 октября 2011

Да, вы должны использовать суффикс f.Причины включают в себя:

  1. Производительность.Когда вы пишете float foo(float x) { return x*3.14; }, вы заставляете компилятор выдавать код, который преобразует x в удвоенное значение, затем выполняет умножение, а затем преобразовывает результат обратно в одиночное.Если вы добавите суффикс f, то оба преобразования будут исключены.На многих платформах каждое из этих преобразований примерно так же дорого, как и само умножение.

  2. Производительность (продолжение).Существуют платформы (например, большинство мобильных телефонов), на которых арифметика с двойной точностью значительно медленнее, чем с одинарной точностью.Даже не обращая внимания на издержки преобразования (описанные в 1.), каждый раз, когда вы заставляете вычисление вычисляться дважды, вы замедляете свою программу.Это не просто «теоретическая» проблема.

  3. Уменьшите риск ошибок.Рассмотрим пример float x = 1.2; if (x == 1.2) // something; Выполнено ли something?Нет, это не так, потому что x содержит 1.2, округленное до float, но сравнивается со значением двойной точности 1.2.Два не равны.

9 голосов
/ 05 октября 2011

Я подозреваю, что-то вроде этого: если вы работаете с переменной с плавающей точкой и двойным литералом, вся операция будет выполнена как двойная, а затем преобразована обратно в число с плавающей точкой.

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

8 голосов
/ 05 октября 2011

Я сделал тест.

Я скомпилировал этот код:

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 результаты совпадают.

3 голосов
/ 05 октября 2011

Как правило, я не думаю, что это будет иметь какое-либо значение, но стоит отметить, что 3.1415f и 3.1415 (как правило) не равны.С другой стороны, вы все равно обычно не делаете никаких вычислений в float, по крайней мере на обычных платформах.(double так же быстро, если не быстрее.) Единственный раз, когда вы должны увидеть float, это когда есть большие массивы, и даже тогда все вычисления обычно будут выполняться в double.

1 голос
/ 05 октября 2011

Из C ++ Standard (рабочий проект) , раздел 5 о бинарных операторах

Многие бинарные операторы, которые ожидают арифметические операнды или Тип перечисления вызывает преобразования и приводит к типам результата в аналогичном путь. Цель состоит в том, чтобы получить общий тип, который также является типом результат. Эта модель называется обычными арифметическими преобразованиями, которые определяются следующим образом: - Если любой из операндов имеет область видимости тип перечисления (7.2), преобразования не выполняются; если другой операнд не имеет того же типа, выражение плохо сформировано. - Если один из операндов имеет тип long double, другой должен быть преобразован долго вдвое. - В противном случае, если один из операндов является двойным, другой должны быть преобразованы в двойной. - В противном случае, если любой из операндов является плавающим, другой должен быть преобразован в плавающее.

А также раздел 4.8

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

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

1 голос
/ 05 октября 2011

Лично я склонен использовать обозначение f postfix как принцип и сделать все возможное, чтобы понять, что это тип float, а не double.

Мои два цента

1 голос
/ 05 октября 2011

Существует разница: если вы используете двойную константу и умножаете ее на переменную с плавающей точкой, сначала переменная преобразуется в двойную, вычисление выполняется в двойную, а затем результат конвертируется в переменную. Хотя точность на самом деле не является проблемой, это может привести к путанице.

...