我们是否应该通常使用浮点数文字而不是更简单的双面文字

Should we generally use float literals for floats instead of the simpler double literals?

本文关键字:文字 更简单 我们 常使用 浮点数 是否      更新时间:2023-10-16

在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后缀?为什么?

引用一个我含蓄地认为理所当然的答案:

如果您正在使用浮点变量和双精度文字,则整体 操作将作为双精度完成,然后转换回浮点数。

这会有什么坏处吗?(除了非常非常理论上的性能影响?

进一步编辑:如果包含技术细节的答案(感谢!(也可以包括这些差异如何影响通用代码,那就太好了。(是的,如果你正在处理数字,你可能希望确保你的big-n浮点运算尽可能高效(和正确(——但对于被调用几次的通用代码来说,这重要吗?如果代码只使用0.0并跳过 - 难以维护,这不是更干净吗!-- 浮动后缀?

是的,您应该使用f后缀。 原因包括:

  1. 性能。 当你编写float foo(float x) { return x*3.14; }时,你强制编译器发出将x转换为双精度的代码,然后进行乘法,然后将结果转换回单。 如果添加f后缀,则会消除这两种转换。 在许多平台上,每次转换都与乘法本身一样昂贵。

  2. 性能(续(。 在某些平台上(例如大多数手机(,双精度算术比单精度算法慢得多。 即使忽略转换开销(在 1. 中介绍(,每次强制以 double 计算计算时,都会减慢程序的速度。 这不仅仅是一个"理论"问题。

  3. 减少您接触错误的风险。 考虑float x = 1.2; if (x == 1.2) // something; something执行的示例? 不,不是,因为 x 保持1.2四舍五入到float,但与双精度值1.2进行比较。 两者并不相等。

我做了一个测试。

我编译了这段代码:

float f1(float x) { return x*3.14; }            
float f2(float x) { return x*3.14F; }   

将 gcc 4.5.1 用于带有优化 -O2 的 i686

这是为 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.1415f3.1415(通常(不相等。 上另一方面,您通常不会在float中进行任何计算无论如何,至少在通常的平台上。 ( double同样快,如果不快。 关于你应该看到float的唯一时间是在那里是大型数组,即使这样,所有计算通常也会在double完成。

有一个区别:如果使用双精度常量并将其与浮点变量相乘,则首先将变量转换为双精度,以双精度完成计算,然后将结果转换为浮点数。虽然精度在这里并不是真正的问题,但这可能会导致混淆。

我个人倾向于使用 f 后缀表示法作为原则问题,并尽可能明显地表明这是一个浮点类型而不是双精度类型。

我的两分钱

摘自C++标准(工作草案(,关于二元运算符的第5节

许多期望算术操作数或 枚举类型导致转换和产生结果类型类似 道路。目的是产生一个通用类型,这也是 结果。这种模式称为通常的算术转换, 定义如下: — 如果任一操作数的作用域为 枚举类型 (7.2(,不执行转换;如果其他 操作数没有相同的类型,表达式格式不正确。— 如果其中一个操作数的类型为长双精度,则应转换另一个操作数 长双倍。— 否则,如果其中一个操作数是双精度数,则另一个操作数是双精度数 应转换为双倍。— 否则,如果任一操作数是浮点数, 另一个应转换为浮动。

还有第 4.8 节

浮点类型的 prvalue 可以转换为 另一种浮点类型。如果源值可以完全 在目标类型中表示,转换的结果是 确切的表示。如果源值位于两个相邻值之间 目标值,则转换结果为 实现定义的任一值的选择。否则, 行为未定义

这样做的结果是,您可以通过以目标类型规定的精度指定常量来避免不必要的转换,前提是您不会因此在计算中失去精度(即,您的操作数在目标类型的精度中完全可表示(。